From 1770ba9a25958489c72b427d174c4bb4157a6f9a Mon Sep 17 00:00:00 2001 From: Dmitry Voropaev Date: Thu, 8 Oct 2026 15:13:50 +0300 Subject: [PATCH] Read ld.so.conf the way ldconfig reads it processLdsoconfFile decides which directories the container's ldcache already covers, so a line it reads differently from ldconfig costs a directory, and the libraries in it are not found. Five differences, against parse_conf and parse_conf_include in glibc's elf/ldconfig.c: - a trailing comment stayed on the line. ldconfig ends every line at the first '#', the format has no quoting; here only a line that started with one was skipped, so "/usr/local/lib # vendor libs" became a directory of that whole name, and the same comment on an include left it matching nothing - "include" followed by a tab was not a directive, since the prefix tested for was "include "; ldconfig takes any blank, so the line became a directory instead - only the first pattern of an include was used: ldconfig splits the remainder on blanks and globs each one, while the whole remainder went to one Glob call, which a pattern with a space in it cannot match - a relative include resolved against the working directory. ldconfig resolves it against the directory of the file that holds it, so after the pivot into the container root "include ld.so.conf.d/*.conf" looked for /ld.so.conf.d/*.conf rather than /etc/ld.so.conf.d/*.conf - a hwcap line became a directory; ldconfig has ignored the directive since 2.33 and only reports it Also answers the TODO on the ErrBadPattern path: ldconfig treats a malformed pattern as no match and carries on, which is what this already did. Signed-off-by: Dmitry Voropaev --- internal/ldconfig/ldconfig.go | 81 +++++++++++++++++--- internal/ldconfig/ldconfig_test.go | 118 +++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 12 deletions(-) diff --git a/internal/ldconfig/ldconfig.go b/internal/ldconfig/ldconfig.go index 72e217740..ae3b18935 100644 --- a/internal/ldconfig/ldconfig.go +++ b/internal/ldconfig/ldconfig.go @@ -27,6 +27,7 @@ import ( "path/filepath" "runtime" "strings" + "unicode" "github.com/prometheus/procfs" @@ -353,6 +354,9 @@ func (l *Ldconfig) getSystemSearchPaths() []string { // processLdsoconfFile extracts the list of directories and included configs // from the specified file. +// +// The file is read the way ldconfig reads it, see parse_conf and +// parse_conf_include in glibc's elf/ldconfig.c. func processLdsoconfFile(ldsoconfFilename string) ([]string, []string, error) { ldsoconf, err := os.Open(ldsoconfFilename) if os.IsNotExist(err) { @@ -367,25 +371,78 @@ func processLdsoconfFile(ldsoconfFilename string) ([]string, []string, error) { var includedFilenames []string scanner := bufio.NewScanner(ldsoconf) for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - switch { - case strings.HasPrefix(line, "#") || len(line) == 0: + // The format has no quoting, so ldconfig ends each line at the first + // '#' instead of only skipping lines that start with one. + line := scanner.Text() + if i := strings.IndexByte(line, '#'); i >= 0 { + line = line[:i] + } + // ldconfig removes leading whitespace and ignores the line if nothing + // is left. + line = strings.TrimLeftFunc(line, unicode.IsSpace) + if strings.TrimSpace(line) == "" { continue - case strings.HasPrefix(line, "include "): - include, err := filepath.Glob(strings.TrimPrefix(line, "include ")) - if err != nil { - // We ignore invalid includes. - // TODO: How does ldconfig handle this? - continue + } + + if patterns, ok := ldsoconfDirective(line, "include"); ok { + for _, pattern := range strings.FieldsFunc(patterns, isBlank) { + include, err := filepath.Glob(resolveIncludePattern(ldsoconfFilename, pattern)) + if err != nil { + // Only ErrBadPattern. ldconfig reports a malformed pattern + // as no match and carries on. + continue + } + includedFilenames = append(includedFilenames, include...) } - includedFilenames = append(includedFilenames, include...) - default: - directories = append(directories, line) + continue } + if _, ok := ldsoconfDirective(line, "hwcap"); ok { + // Not a directory. ldconfig has ignored this directive since 2.33 + // and only reports it. + continue + } + + directories = append(directories, strings.TrimSpace(line)) } return directories, includedFilenames, nil } +// isBlank matches the characters that separate the patterns of an include +// directive, which are the ones C's isblank matches in the POSIX locale. +func isBlank(r rune) bool { + return r == ' ' || r == '\t' +} + +// ldsoconfDirective returns the remainder of the line if it names the specified +// directive followed by a blank. ldconfig compares `include` as-is and `hwcap` +// without regard to case. +func ldsoconfDirective(line string, directive string) (string, bool) { + if len(line) <= len(directive) { + return "", false + } + name, rest := line[:len(directive)], line[len(directive):] + if directive == "hwcap" { + if !strings.EqualFold(name, directive) { + return "", false + } + } else if name != directive { + return "", false + } + if !isBlank(rune(rest[0])) { + return "", false + } + return rest[1:], true +} + +// resolveIncludePattern resolves a relative include pattern against the +// directory of the file that holds it, as ldconfig does. +func resolveIncludePattern(ldsoconfFilename string, pattern string) string { + if filepath.IsAbs(pattern) || !strings.Contains(ldsoconfFilename, "/") { + return pattern + } + return filepath.Join(filepath.Dir(ldsoconfFilename), pattern) +} + // createMuslPathFileIfRequired creates a musl .path file that allows libraries // from the specified directories to be discovered on the system. // This is required because systems that use musl do not rely on the ldcache to diff --git a/internal/ldconfig/ldconfig_test.go b/internal/ldconfig/ldconfig_test.go index c983e7f4f..88bfd2ecf 100644 --- a/internal/ldconfig/ldconfig_test.go +++ b/internal/ldconfig/ldconfig_test.go @@ -246,3 +246,121 @@ func TestEnsureLdsoconfFile(t *testing.T) { }) } } + +func TestProcessLdsoconfFile(t *testing.T) { + testCases := []struct { + description string + // files are created relative to a temporary directory. The entry + // "ld.so.conf" is the file that is read. + files map[string]string + expectedDirectories []string + expectedIncludes []string + }{ + { + description: "a comment at the end of a line is not part of the directory", + files: map[string]string{ + "ld.so.conf": "/usr/local/lib # vendor libraries\n/opt/lib\t# and these\n", + }, + expectedDirectories: []string{"/usr/local/lib", "/opt/lib"}, + }, + { + description: "a comment at the end of an include is not part of the pattern", + files: map[string]string{ + "ld.so.conf": "include ld.so.conf.d/*.conf # the distro's\n", + "ld.so.conf.d/a.conf": "/dir-a\n", + }, + expectedIncludes: []string{"ld.so.conf.d/a.conf"}, + }, + { + description: "a line that is only a comment is ignored", + files: map[string]string{ + "ld.so.conf": "# nothing here\n # nor here\n/dir-a\n", + }, + expectedDirectories: []string{"/dir-a"}, + }, + { + description: "an include separated by a tab is a directive", + files: map[string]string{ + "ld.so.conf": "include\tld.so.conf.d/*.conf\n", + "ld.so.conf.d/a.conf": "/dir-a\n", + }, + expectedIncludes: []string{"ld.so.conf.d/a.conf"}, + }, + { + description: "every pattern of an include is used", + files: map[string]string{ + "ld.so.conf": "include first.conf \t second.conf third.conf\n", + "first.conf": "/dir-a\n", + "second.conf": "/dir-b\n", + "third.conf": "/dir-c\n", + }, + expectedIncludes: []string{"first.conf", "second.conf", "third.conf"}, + }, + { + description: "a relative include resolves against the directory of the file", + files: map[string]string{ + "etc/ld.so.conf": "include ld.so.conf.d/*.conf\n", + "etc/ld.so.conf.d/a.conf": "/dir-a\n", + }, + expectedIncludes: []string{"etc/ld.so.conf.d/a.conf"}, + }, + { + description: "an absolute include is used as it is", + files: map[string]string{ + "ld.so.conf": "include {{TMPDIR}}/other.conf\n", + "other.conf": "/dir-a\n", + }, + expectedIncludes: []string{"other.conf"}, + }, + { + description: "a hwcap directive is not a directory", + files: map[string]string{ + "ld.so.conf": "hwcap 1 nosegneg\nHWCAP 0 something\n/dir-a\n", + }, + expectedDirectories: []string{"/dir-a"}, + }, + { + description: "a line that only looks like a directive is a directory", + files: map[string]string{ + "ld.so.conf": "include\n/includes\n/hwcapsomething\n", + }, + expectedDirectories: []string{"include", "/includes", "/hwcapsomething"}, + }, + { + description: "an include that matches nothing is not an error", + files: map[string]string{ + "ld.so.conf": "include ld.so.conf.d/*.conf\n/dir-a\n", + }, + expectedDirectories: []string{"/dir-a"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + tmpDir := t.TempDir() + + var configFile string + for name, contents := range tc.files { + path := filepath.Join(tmpDir, name) + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755)) + contents = strings.ReplaceAll(contents, "{{TMPDIR}}", tmpDir) + require.NoError(t, os.WriteFile(path, []byte(contents), 0600)) + if filepath.Base(name) == "ld.so.conf" { + configFile = path + } + } + require.NotEmpty(t, configFile) + + directories, includes, err := processLdsoconfFile(configFile) + require.NoError(t, err) + + var expectedIncludes []string + for _, include := range tc.expectedIncludes { + expectedIncludes = append(expectedIncludes, filepath.Join(tmpDir, include)) + } + + require.EqualValues(t, tc.expectedDirectories, directories) + require.EqualValues(t, expectedIncludes, includes) + }) + } +}