Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 69 additions & 12 deletions internal/ldconfig/ldconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"path/filepath"
"runtime"
"strings"
"unicode"

"github.com/prometheus/procfs"

Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
118 changes: 118 additions & 0 deletions internal/ldconfig/ldconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}