Skip to content

Commit 1bd5242

Browse files
committed
Filter already tracked directories from ldcache update
This change fixes the behavior where the order of precedence of existing folders are changed because they are added to the .conf file for ldconfig. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 3fe923d commit 1bd5242

File tree

3 files changed

+317
-10
lines changed

3 files changed

+317
-10
lines changed

internal/ldconfig/ldconfig.go

Lines changed: 164 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
package ldconfig
1919

2020
import (
21+
"bufio"
2122
"flag"
2223
"fmt"
2324
"os"
2425
"os/exec"
2526
"path/filepath"
27+
"runtime"
2628
"strings"
2729

2830
"github.com/NVIDIA/nvidia-container-toolkit/internal/config"
@@ -38,9 +40,11 @@ const (
3840
)
3941

4042
type Ldconfig struct {
41-
ldconfigPath string
42-
inRoot string
43-
directories []string
43+
ldconfigPath string
44+
inRoot string
45+
isDebianLikeHost bool
46+
isDebianLikeContainer bool
47+
directories []string
4448
}
4549

4650
// NewRunner creates an exec.Cmd that can be used to run ldconfig.
@@ -50,6 +54,9 @@ func NewRunner(id string, ldconfigPath string, containerRoot string, additionala
5054
"--ldconfig-path", strings.TrimPrefix(config.NormalizeLDConfigPath("@"+ldconfigPath), "@"),
5155
"--container-root", containerRoot,
5256
}
57+
if isDebian() {
58+
args = append(args, "--is-debian-like-host")
59+
}
5360
args = append(args, additionalargs...)
5461

5562
return createReexecCommand(args)
@@ -66,6 +73,10 @@ func NewRunner(id string, ldconfigPath string, containerRoot string, additionala
6673
// --ldconfig-path=LDCONFIG_PATH the path to ldconfig on the host
6774
// --container-root=CONTAINER_ROOT the path in which ldconfig must be run
6875
//
76+
// The following flags are optional:
77+
//
78+
// --is-debian-like-host Indicates that the host system is debian-based.
79+
//
6980
// The remaining args are folders where soname symlinks need to be created.
7081
func NewFromArgs(args ...string) (*Ldconfig, error) {
7182
if len(args) < 1 {
@@ -74,6 +85,7 @@ func NewFromArgs(args ...string) (*Ldconfig, error) {
7485
fs := flag.NewFlagSet(args[1], flag.ExitOnError)
7586
ldconfigPath := fs.String("ldconfig-path", "", "the path to ldconfig on the host")
7687
containerRoot := fs.String("container-root", "", "the path in which ldconfig must be run")
88+
isDebianLikeHost := fs.Bool("is-debian-like-host", false, "the hook is running from a Debian-like host")
7789
if err := fs.Parse(args[1:]); err != nil {
7890
return nil, err
7991
}
@@ -86,9 +98,11 @@ func NewFromArgs(args ...string) (*Ldconfig, error) {
8698
}
8799

88100
l := &Ldconfig{
89-
ldconfigPath: *ldconfigPath,
90-
inRoot: *containerRoot,
91-
directories: fs.Args(),
101+
ldconfigPath: *ldconfigPath,
102+
inRoot: *containerRoot,
103+
isDebianLikeHost: *isDebianLikeHost,
104+
isDebianLikeContainer: isDebian(),
105+
directories: fs.Args(),
92106
}
93107
return l, nil
94108
}
@@ -99,15 +113,21 @@ func (l *Ldconfig) UpdateLDCache() error {
99113
return err
100114
}
101115

116+
// Explicitly specify using /etc/ld.so.conf since the host's ldconfig may
117+
// be configured to use a different config file by default.
118+
const topLevelLdsoconfFilePath = "/etc/ld.so.conf"
119+
filteredDirectories, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...)
120+
if err != nil {
121+
return err
122+
}
123+
102124
args := []string{
103125
filepath.Base(ldconfigPath),
104-
// Explicitly specify using /etc/ld.so.conf since the host's ldconfig may
105-
// be configured to use a different config file by default.
106-
"-f", "/etc/ld.so.conf",
126+
"-f", topLevelLdsoconfFilePath,
107127
"-C", "/etc/ld.so.cache",
108128
}
109129

110-
if err := createLdsoconfdFile(ldsoconfdFilenamePattern, l.directories...); err != nil {
130+
if err := createLdsoconfdFile(ldsoconfdFilenamePattern, filteredDirectories...); err != nil {
111131
return fmt.Errorf("failed to update ld.so.conf.d: %w", err)
112132
}
113133

@@ -137,6 +157,22 @@ func (l *Ldconfig) prepareRoot() (string, error) {
137157
return ldconfigPath, nil
138158
}
139159

160+
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, error) {
161+
ldconfigDirs, err := l.getLdsoconfDirectories(configFilePath)
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
var filtered []string
167+
for _, d := range directories {
168+
if _, ok := ldconfigDirs[d]; ok {
169+
continue
170+
}
171+
filtered = append(filtered, d)
172+
}
173+
return filtered, nil
174+
}
175+
140176
// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/.
141177
// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and
142178
// contains the specified directories on each line.
@@ -177,3 +213,121 @@ func createLdsoconfdFile(pattern string, dirs ...string) error {
177213

178214
return nil
179215
}
216+
217+
// getLdsoconfDirectories returns a map of ldsoconf directories to the conf
218+
// files that refer to the directory.
219+
func (l *Ldconfig) getLdsoconfDirectories(configFilePath string) (map[string]struct{}, error) {
220+
ldconfigDirs := make(map[string]struct{})
221+
for _, d := range l.getSystemSerachPaths() {
222+
ldconfigDirs[d] = struct{}{}
223+
}
224+
225+
processedConfFiles := make(map[string]bool)
226+
ldsoconfFilenames := []string{configFilePath}
227+
for len(ldsoconfFilenames) > 0 {
228+
ldsoconfFilename := ldsoconfFilenames[0]
229+
ldsoconfFilenames = ldsoconfFilenames[1:]
230+
if processedConfFiles[ldsoconfFilename] {
231+
continue
232+
}
233+
processedConfFiles[ldsoconfFilename] = true
234+
235+
if len(ldsoconfFilename) == 0 {
236+
continue
237+
}
238+
directories, includedFilenames, err := processLdsoconfFile(ldsoconfFilename)
239+
if err != nil {
240+
return nil, err
241+
}
242+
ldsoconfFilenames = append(ldsoconfFilenames, includedFilenames...)
243+
for _, d := range directories {
244+
ldconfigDirs[d] = struct{}{}
245+
}
246+
}
247+
return ldconfigDirs, nil
248+
}
249+
250+
func (l *Ldconfig) getSystemSerachPaths() []string {
251+
if l.isDebianLikeContainer {
252+
debianSystemSearchPaths()
253+
}
254+
return nonDebianSystemSearchPaths()
255+
}
256+
257+
// processLdsoconfFile extracts the list of directories and included configs
258+
// from the specified file.
259+
func processLdsoconfFile(ldsoconfFilename string) ([]string, []string, error) {
260+
ldsoconf, err := os.Open(ldsoconfFilename)
261+
if os.IsNotExist(err) {
262+
return nil, nil, nil
263+
}
264+
if err != nil {
265+
return nil, nil, err
266+
}
267+
defer ldsoconf.Close()
268+
269+
var directories []string
270+
var includedFilenames []string
271+
scanner := bufio.NewScanner(ldsoconf)
272+
for scanner.Scan() {
273+
line := strings.TrimSpace(scanner.Text())
274+
switch {
275+
case strings.HasPrefix(line, "#") || len(line) == 0:
276+
continue
277+
case strings.HasPrefix(line, "include "):
278+
include, err := filepath.Glob(strings.TrimPrefix(line, "include "))
279+
if err != nil {
280+
// We ignore invalid includes.
281+
// TODO: How does ldconfig handle this?
282+
continue
283+
}
284+
includedFilenames = append(includedFilenames, include...)
285+
default:
286+
directories = append(directories, line)
287+
}
288+
}
289+
return directories, includedFilenames, nil
290+
}
291+
292+
func isDebian() bool {
293+
info, err := os.Stat("/etc/debian_version")
294+
if err != nil {
295+
return false
296+
}
297+
return !info.IsDir()
298+
}
299+
300+
// nonDebianSystemSearchPaths returns the system search paths for non-Debian
301+
// systems.
302+
//
303+
// This list was taken from the output of:
304+
//
305+
// docker run --rm -ti redhat/ubi9 /usr/lib/ld-linux-aarch64.so.1 --help | grep -A6 "Shared library search path"
306+
func nonDebianSystemSearchPaths() []string {
307+
return []string{"/lib64", "/usr/lib64"}
308+
}
309+
310+
// debianSystemSearchPaths returns the system search paths for Debian-like
311+
// systems.
312+
//
313+
// This list was taken from the output of:
314+
//
315+
// docker run --rm -ti ubuntu /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 --help | grep -A6 "Shared library search path"
316+
func debianSystemSearchPaths() []string {
317+
var paths []string
318+
switch runtime.GOARCH {
319+
case "amd64":
320+
paths = append(paths,
321+
"/lib/x86_64-linux-gnu",
322+
"/usr/lib/x86_64-linux-gnu",
323+
)
324+
case "arm64":
325+
paths = append(paths,
326+
"/lib/aarch64-linux-gnu",
327+
"/usr/lib/aarch64-linux-gnu",
328+
)
329+
}
330+
paths = append(paths, "/lib", "/usr/lib")
331+
332+
return paths
333+
}

internal/ldconfig/ldconfig_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
**/
17+
18+
package ldconfig
19+
20+
import (
21+
"os"
22+
"strings"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestFilterDirectories(t *testing.T) {
29+
const topLevelConf = "TOPLEVEL.conf"
30+
31+
testCases := []struct {
32+
description string
33+
confs map[string]string // map[filename]content, must have topLevelConf key
34+
input []string
35+
expected []string
36+
}{
37+
{
38+
description: "all filtered",
39+
confs: map[string]string{
40+
topLevelConf: `
41+
# some comment
42+
/tmp/libdir1
43+
/tmp/libdir2
44+
`,
45+
},
46+
input: []string{"/tmp/libdir1", "/tmp/libdir2"},
47+
expected: nil,
48+
},
49+
{
50+
description: "partially filtered",
51+
confs: map[string]string{
52+
topLevelConf: `
53+
/tmp/libdir1
54+
`,
55+
},
56+
input: []string{"/tmp/libdir1", "/tmp/libdir2"},
57+
expected: []string{"/tmp/libdir2"},
58+
},
59+
{
60+
description: "none filtered",
61+
confs: map[string]string{
62+
topLevelConf: `
63+
# empty config
64+
`,
65+
},
66+
input: []string{"/tmp/libdir1", "/tmp/libdir2"},
67+
expected: []string{"/tmp/libdir1", "/tmp/libdir2"},
68+
},
69+
{
70+
description: "filter with include and comments",
71+
confs: map[string]string{
72+
topLevelConf: `
73+
# comment
74+
/tmp/libdir1
75+
include /nonexistent/pattern*
76+
`,
77+
},
78+
input: []string{"/tmp/libdir1", "/tmp/libdir2"},
79+
expected: []string{"/tmp/libdir2"},
80+
},
81+
{
82+
description: "include directive picks up more dirs to filter",
83+
confs: map[string]string{
84+
topLevelConf: `
85+
# top-level
86+
include INCLUDED_PATTERN*
87+
/tmp/libdir3
88+
`,
89+
"INCLUDED_PATTERN0.conf": `
90+
/tmp/libdir2
91+
# another comment
92+
/tmp/libdir4
93+
`,
94+
"INCLUDED_PATTERN1.conf": `
95+
/tmp/libdir1
96+
`,
97+
},
98+
input: []string{"/tmp/libdir1", "/tmp/libdir2", "/tmp/libdir3", "/tmp/libdir4", "/tmp/libdir5"},
99+
expected: []string{"/tmp/libdir5"},
100+
},
101+
}
102+
103+
for _, tc := range testCases {
104+
t.Run(tc.description, func(t *testing.T) {
105+
tmpDir := t.TempDir()
106+
107+
// Prepare file contents, adjusting include globs to be absolute and unique within tmpDir
108+
for name, content := range tc.confs {
109+
if name == topLevelConf && len(tc.confs) > 1 {
110+
content = strings.ReplaceAll(content, "include INCLUDED_PATTERN*", "include "+tmpDir+"/INCLUDED_PATTERN*")
111+
}
112+
err := os.WriteFile(tmpDir+"/"+name, []byte(content), 0600)
113+
require.NoError(t, err)
114+
}
115+
116+
topLevelConfPath := tmpDir + "/" + topLevelConf
117+
l := &Ldconfig{
118+
isDebianLikeContainer: true,
119+
}
120+
filtered, err := l.filterDirectories(topLevelConfPath, tc.input...)
121+
122+
require.NoError(t, err)
123+
require.Equal(t, tc.expected, filtered)
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)