Skip to content

Commit 5de1fb0

Browse files
committed
Write system paths to lexicographically last ld.so.conf.d drop-in
In most cases, the hook will be executing a host ldconfig that may be configured widely differently from what the container image expects. The common case is Debian vs non-Debian. But there are also hosts that configure ldconfig to search in a glibc prefix (e.g. /usr/lib/glibc). To avoid all these cases, write the container's expected system search paths to a drop-in conf file that is likely to be last in lexicographic order. Entries in the top-level ld.so.conf file may be processed after this drop-in, but this hook does not modify the top-level file if it exists. Signed-off-by: Jean-Francois Roy <[email protected]>
1 parent 37ab9d1 commit 5de1fb0

File tree

2 files changed

+156
-29
lines changed

2 files changed

+156
-29
lines changed

internal/ldconfig/ldconfig.go

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ const (
3737
// higher precedence than other libraries on the system, but lower than
3838
// the 00-cuda-compat that is included in some containers.
3939
ldsoconfdFilenamePattern = "00-nvcr-*.conf"
40+
// ldsoconfdSystemDirsFilenamePattern specifies the filename pattern for the drop-in conf file
41+
// that includes the expected system directories for the container.
42+
ldsoconfdSystemDirsFilenamePattern = "99-nvcr-*.conf"
4043
)
4144

4245
type Ldconfig struct {
@@ -123,7 +126,7 @@ func (l *Ldconfig) UpdateLDCache() error {
123126
// Explicitly specify using /etc/ld.so.conf since the host's ldconfig may
124127
// be configured to use a different config file by default.
125128
const topLevelLdsoconfFilePath = "/etc/ld.so.conf"
126-
filteredDirectories, ldconfigDirs, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...)
129+
filteredDirectories, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...)
127130
if err != nil {
128131
return err
129132
}
@@ -133,22 +136,20 @@ func (l *Ldconfig) UpdateLDCache() error {
133136
"-f", topLevelLdsoconfFilePath,
134137
"-C", "/etc/ld.so.cache",
135138
}
136-
// If we are running in a non-debian container on a debian host we also
137-
// need to add the system directories for non-debian hosts to the list of
138-
// folders processed by ldconfig.
139-
// We only do this if they are not already tracked, since the folders on
140-
// on the command line have a higher priority than folders in ld.so.conf.
141-
if l.isDebianLikeHost && !l.isDebianLikeContainer {
142-
for _, systemSearchPath := range l.getSystemSearchPaths() {
143-
if _, ok := ldconfigDirs[systemSearchPath]; ok {
144-
continue
145-
}
146-
args = append(args, "/lib64", "/usr/lib64")
147-
}
148-
}
149139

150140
if err := createLdsoconfdFile(ldsoconfdFilenamePattern, filteredDirectories...); err != nil {
151-
return fmt.Errorf("failed to update ld.so.conf.d: %w", err)
141+
return fmt.Errorf("failed to write %s drop-in: %w", ldsoconfdFilenamePattern, err)
142+
}
143+
144+
// In most cases, the hook will be executing a host ldconfig that may be configured widely
145+
// differently from what the container image expects. The common case is Debian vs non-Debian.
146+
// But there are also hosts that configure ldconfig to search in a glibc prefix
147+
// (e.g. /usr/lib/glibc). To avoid all these cases, write the container's expected system search
148+
// paths to a drop-in conf file that is likely to be last in lexicographic order. Entries in the
149+
// top-level ld.so.conf file may be processed after this drop-in, but this hook does not modify
150+
// the top-level file if it exists.
151+
if err := createLdsoconfdFile(ldsoconfdSystemDirsFilenamePattern, l.getSystemSearchPaths()...); err != nil {
152+
return fmt.Errorf("failed to write %s drop-in: %w", ldsoconfdSystemDirsFilenamePattern, err)
152153
}
153154

154155
return SafeExec(ldconfigPath, args, nil)
@@ -183,10 +184,10 @@ func (l *Ldconfig) prepareRoot() (string, error) {
183184
return ldconfigPath, nil
184185
}
185186

186-
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, map[string]struct{}, error) {
187+
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, error) {
187188
ldconfigDirs, err := l.getLdsoconfDirectories(configFilePath)
188189
if err != nil {
189-
return nil, nil, err
190+
return nil, err
190191
}
191192

192193
var filtered []string
@@ -197,7 +198,7 @@ func (l *Ldconfig) filterDirectories(configFilePath string, directories ...strin
197198
filtered = append(filtered, d)
198199
ldconfigDirs[d] = struct{}{}
199200
}
200-
return filtered, ldconfigDirs, nil
201+
return filtered, nil
201202
}
202203

203204
// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/.
@@ -241,6 +242,24 @@ func createLdsoconfdFile(pattern string, dirs ...string) error {
241242
return nil
242243
}
243244

245+
func appendSystemSearchPathsToLdsoconf(configFilePath string, dirs ...string) error {
246+
if len(dirs) == 0 {
247+
return nil
248+
}
249+
configFile, err := os.OpenFile(configFilePath, os.O_APPEND|os.O_WRONLY, 0644)
250+
if err != nil {
251+
return fmt.Errorf("failed to open config file: %w", err)
252+
}
253+
defer configFile.Close()
254+
for _, dir := range dirs {
255+
_, err = fmt.Fprintf(configFile, "%s\n", dir)
256+
if err != nil {
257+
return fmt.Errorf("failed to update config file: %w", err)
258+
}
259+
}
260+
return nil
261+
}
262+
244263
// getLdsoconfDirectories returns a map of ldsoconf directories to the conf
245264
// files that refer to the directory.
246265
func (l *Ldconfig) getLdsoconfDirectories(configFilePath string) (map[string]struct{}, error) {
@@ -324,22 +343,61 @@ func isDebian() bool {
324343
return !info.IsDir()
325344
}
326345

327-
// nonDebianSystemSearchPaths returns the system search paths for non-Debian
328-
// systems.
346+
// nonDebianSystemSearchPaths returns the system search paths for non-Debian systems.
347+
//
348+
// glibc ldconfig's calls `add_system_dir` with `SLIBDIR` and `LIBDIR` (if they are not equal). On
349+
// aarch64 and x86_64, `add_system_dir` is a macro that scans the provided path. If the path ends
350+
// with "/lib64" (or "/libx32", x86_64 only), it strips those suffixes. Then it registers the
351+
// resulting path. Then if the path ends with "/lib", it registers "path"+"64" (and "path"+"x32",
352+
// x86_64 only).
353+
//
354+
// By default, "LIBDIR" is "/usr/lib" and "SLIBDIR" is "/lib". Note that on modern distributions,
355+
// "/lib" is usually a symlink to "/usr/lib" and "/lib64" to "/usr/lib64". ldconfig resolves
356+
// symlinks and skips duplicate directory entries.
357+
//
358+
// To get the list of system paths, you can invoke the dynamic linker with `--list-diagnostics` and
359+
// look for "path.system_dirs". For example
360+
// `docker run --rm -ti fedora:latest /lib64/ld-linux-x86-64.so.2 --list-diagnostics | grep path.system_dirs`.
329361
//
330-
// This list was taken from the output of:
362+
// On most distributions, including Fedora and derivatives, this yields the following
363+
// ldconfig system search paths.
331364
//
332-
// docker run --rm -ti redhat/ubi9 /usr/lib/ld-linux-aarch64.so.1 --help | grep -A6 "Shared library search path"
365+
// TODO: Add other architectures that have custom `add_system_dir` macros (e.g. riscv)
366+
// TODO: Replace with executing the container's dynamlic linker with `--list-diagnostics`?
333367
func nonDebianSystemSearchPaths() []string {
334-
return []string{"/lib64", "/usr/lib64"}
368+
var paths []string
369+
paths = append(paths, "/lib", "/usr/lib")
370+
switch runtime.GOARCH {
371+
case "amd64":
372+
paths = append(paths,
373+
"/lib/lib64",
374+
"/usr/lib64",
375+
"/libx32",
376+
"/usr/libx32",
377+
)
378+
case "arm64":
379+
paths = append(paths,
380+
"/lib/lib64",
381+
"/usr/lib64",
382+
)
383+
}
384+
return paths
335385
}
336386

337-
// debianSystemSearchPaths returns the system search paths for Debian-like
338-
// systems.
387+
// debianSystemSearchPaths returns the system search paths for Debian-like systems.
388+
//
389+
// Debian (and derivatives) apply their multi-arch patch to glibc, which modifies ldconfig to
390+
// use the same set of system paths as the dynamic linker. These paths are going to include the
391+
// multi-arch directory _and_ by default "/lib" and "/usr/lib" for compatibility.
339392
//
340-
// This list was taken from the output of:
393+
// To get the list of system paths, you can invoke the dynamic linker with `--list-diagnostics` and
394+
// look for "path.system_dirs". For example
395+
// `docker run --rm -ti ubuntu:latest /lib64/ld-linux-x86-64.so.2 --list-diagnostics | grep path.system_dirs`.
341396
//
342-
// docker run --rm -ti ubuntu /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 --help | grep -A6 "Shared library search path"
397+
// This yields the following ldconfig system search paths.
398+
//
399+
// TODO: Add other architectures that have custom `add_system_dir` macros (e.g. riscv)
400+
// TODO: Replace with executing the container's dynamlic linker with `--list-diagnostics`?
343401
func debianSystemSearchPaths() []string {
344402
var paths []string
345403
switch runtime.GOARCH {
@@ -355,6 +413,5 @@ func debianSystemSearchPaths() []string {
355413
)
356414
}
357415
paths = append(paths, "/lib", "/usr/lib")
358-
359416
return paths
360417
}

internal/ldconfig/ldconfig_test.go

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package ldconfig
1919

2020
import (
2121
"os"
22+
"path/filepath"
2223
"strings"
2324
"testing"
2425

@@ -117,10 +118,79 @@ include INCLUDED_PATTERN*
117118
l := &Ldconfig{
118119
isDebianLikeContainer: true,
119120
}
120-
filtered, _, err := l.filterDirectories(topLevelConfPath, tc.input...)
121+
filtered, err := l.filterDirectories(topLevelConfPath, tc.input...)
121122

122123
require.NoError(t, err)
123124
require.Equal(t, tc.expected, filtered)
124125
})
125126
}
126127
}
128+
129+
func TestAppendSystemSearchPathsToLdsoconf(t *testing.T) {
130+
testCases := []struct {
131+
description string
132+
initialContent string
133+
noFile bool
134+
dirs []string
135+
expectedFinal string
136+
expectError bool
137+
}{
138+
{
139+
description: "append to empty file",
140+
initialContent: "",
141+
dirs: []string{"/lib", "/usr/lib"},
142+
expectedFinal: "/lib\n/usr/lib\n",
143+
},
144+
{
145+
description: "append to existing content",
146+
initialContent: "# existing config\n/existing/path\n",
147+
dirs: []string{"/lib", "/usr/lib"},
148+
expectedFinal: "# existing config\n/existing/path\n/lib\n/usr/lib\n",
149+
},
150+
{
151+
description: "append empty does nothing",
152+
initialContent: "# existing config\n",
153+
dirs: []string{},
154+
expectedFinal: "# existing config\n",
155+
},
156+
{
157+
description: "append empty does not create file",
158+
noFile: true,
159+
dirs: []string{},
160+
},
161+
{
162+
description: "append to non-existent file fails",
163+
noFile: true,
164+
dirs: []string{"/lib"},
165+
expectError: true,
166+
},
167+
}
168+
169+
for _, tc := range testCases {
170+
t.Run(tc.description, func(t *testing.T) {
171+
tmpDir := t.TempDir()
172+
configPath := filepath.Join(tmpDir, "ld.so.conf")
173+
174+
if !tc.noFile {
175+
err := os.WriteFile(configPath, []byte(tc.initialContent), 0644)
176+
require.NoError(t, err)
177+
}
178+
179+
err := appendSystemSearchPathsToLdsoconf(configPath, tc.dirs...)
180+
if tc.expectError {
181+
require.Error(t, err)
182+
return
183+
}
184+
require.NoError(t, err)
185+
186+
if !tc.noFile {
187+
content, err := os.ReadFile(configPath)
188+
require.NoError(t, err)
189+
require.Equal(t, tc.expectedFinal, string(content))
190+
} else {
191+
_, err := os.Stat(configPath)
192+
require.True(t, os.IsNotExist(err))
193+
}
194+
})
195+
}
196+
}

0 commit comments

Comments
 (0)