Skip to content

Commit ce85f8d

Browse files
committed
Append system paths at the end of top-level ld.so.conf
1 parent f432507 commit ce85f8d

File tree

2 files changed

+151
-28
lines changed

2 files changed

+151
-28
lines changed

internal/ldconfig/ldconfig.go

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (l *Ldconfig) UpdateLDCache() error {
123123
// Explicitly specify using /etc/ld.so.conf since the host's ldconfig may
124124
// be configured to use a different config file by default.
125125
const topLevelLdsoconfFilePath = "/etc/ld.so.conf"
126-
filteredDirectories, ldconfigDirs, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...)
126+
filteredDirectories, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...)
127127
if err != nil {
128128
return err
129129
}
@@ -133,24 +133,21 @@ func (l *Ldconfig) UpdateLDCache() error {
133133
"-f", topLevelLdsoconfFilePath,
134134
"-C", "/etc/ld.so.cache",
135135
}
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-
}
149136

150137
if err := createLdsoconfdFile(ldsoconfdFilenamePattern, filteredDirectories...); err != nil {
151138
return fmt.Errorf("failed to update ld.so.conf.d: %w", err)
152139
}
153140

141+
// In most cases, the hook will be executing a host ldconfig that may be configured widely
142+
// differently from what the container image expects. The common case is Debian vs non-Debian.
143+
// But there are also hosts that configure ldconfig to search in a glibc prefix
144+
// (e.g. /usr/lib/glibc). To avoid all these cases, append the container's expected system
145+
// search paths to the top-level ld.so.conf. This will ensure they get scanned but won't
146+
// materially change the scan order.
147+
if err := appendSystemSearchPathsToLdsoconf(topLevelLdsoconfFilePath, l.getSystemSearchPaths()...); err != nil {
148+
return fmt.Errorf("failed to append system search paths to %s: %w", topLevelLdsoconfFilePath, err)
149+
}
150+
154151
return SafeExec(ldconfigPath, args, nil)
155152
}
156153

@@ -177,10 +174,10 @@ func (l *Ldconfig) prepareRoot() (string, error) {
177174
return ldconfigPath, nil
178175
}
179176

180-
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, map[string]struct{}, error) {
177+
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, error) {
181178
ldconfigDirs, err := l.getLdsoconfDirectories(configFilePath)
182179
if err != nil {
183-
return nil, nil, err
180+
return nil, err
184181
}
185182

186183
var filtered []string
@@ -191,7 +188,7 @@ func (l *Ldconfig) filterDirectories(configFilePath string, directories ...strin
191188
filtered = append(filtered, d)
192189
ldconfigDirs[d] = struct{}{}
193190
}
194-
return filtered, ldconfigDirs, nil
191+
return filtered, nil
195192
}
196193

197194
// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/.
@@ -235,6 +232,24 @@ func createLdsoconfdFile(pattern string, dirs ...string) error {
235232
return nil
236233
}
237234

235+
func appendSystemSearchPathsToLdsoconf(configFilePath string, dirs ...string) error {
236+
if len(dirs) == 0 {
237+
return nil
238+
}
239+
configFile, err := os.OpenFile(configFilePath, os.O_APPEND|os.O_WRONLY, 0644)
240+
if err != nil {
241+
return fmt.Errorf("failed to open config file: %w", err)
242+
}
243+
defer configFile.Close()
244+
for _, dir := range dirs {
245+
_, err = fmt.Fprintf(configFile, "%s\n", dir)
246+
if err != nil {
247+
return fmt.Errorf("failed to update config file: %w", err)
248+
}
249+
}
250+
return nil
251+
}
252+
238253
// getLdsoconfDirectories returns a map of ldsoconf directories to the conf
239254
// files that refer to the directory.
240255
func (l *Ldconfig) getLdsoconfDirectories(configFilePath string) (map[string]struct{}, error) {
@@ -318,22 +333,61 @@ func isDebian() bool {
318333
return !info.IsDir()
319334
}
320335

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

331-
// debianSystemSearchPaths returns the system search paths for Debian-like
332-
// systems.
377+
// debianSystemSearchPaths returns the system search paths for Debian-like systems.
378+
//
379+
// Debian (and derivatives) apply their multi-arch patch to glibc, which modifies ldconfig to
380+
// use the same set of system paths as the dynamic linker. These paths are going to include the
381+
// multi-arch directory _and_ by default "/lib" and "/usr/lib" for compatibility.
333382
//
334-
// This list was taken from the output of:
383+
// To get the list of system paths, you can invoke the dynamic linker with `--list-diagnostics` and
384+
// look for "path.system_dirs". For example
385+
// `docker run --rm -ti ubuntu:latest /lib64/ld-linux-x86-64.so.2 --list-diagnostics | grep path.system_dirs`.
335386
//
336-
// docker run --rm -ti ubuntu /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 --help | grep -A6 "Shared library search path"
387+
// This yields the following ldconfig system search paths.
388+
//
389+
// TODO: Add other architectures that have custom `add_system_dir` macros (e.g. riscv)
390+
// TODO: Replace with executing the container's dynamlic linker with `--list-diagnostics`?
337391
func debianSystemSearchPaths() []string {
338392
var paths []string
339393
switch runtime.GOARCH {
@@ -349,6 +403,5 @@ func debianSystemSearchPaths() []string {
349403
)
350404
}
351405
paths = append(paths, "/lib", "/usr/lib")
352-
353406
return paths
354407
}

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)