Skip to content

Commit 7805e62

Browse files
jfroyelezar
authored andcommitted
Append system paths at the end of top-level ld.so.conf
Signed-off-by: Evan Lezar <[email protected]>
1 parent 70cca03 commit 7805e62

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, systemSearchPath)
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

@@ -183,10 +180,10 @@ func (l *Ldconfig) prepareRoot() (string, error) {
183180
return ldconfigPath, nil
184181
}
185182

186-
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, map[string]struct{}, error) {
183+
func (l *Ldconfig) filterDirectories(configFilePath string, directories ...string) ([]string, error) {
187184
ldconfigDirs, err := l.getLdsoconfDirectories(configFilePath)
188185
if err != nil {
189-
return nil, nil, err
186+
return nil, err
190187
}
191188

192189
var filtered []string
@@ -197,7 +194,7 @@ func (l *Ldconfig) filterDirectories(configFilePath string, directories ...strin
197194
filtered = append(filtered, d)
198195
ldconfigDirs[d] = struct{}{}
199196
}
200-
return filtered, ldconfigDirs, nil
197+
return filtered, nil
201198
}
202199

203200
// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/.
@@ -241,6 +238,24 @@ func createLdsoconfdFile(pattern string, dirs ...string) error {
241238
return nil
242239
}
243240

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

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

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

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), 0600)
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)