Skip to content

Commit ad7c96a

Browse files
committed
ldconfig: Create ld.so.conf file if missing
Some container images (namely distroless images) may not have the /etc/ld.so.conf file present. This patch modifies the ldconfig CDI hook to create a "standard" top-level ld.so.conf file if it is missing that includes "standard" /etc/ld.so.conf.d/*.conf drop-in files. Signed-off-by: Jean-Francois Roy <[email protected]>
1 parent ce85f8d commit ad7c96a

File tree

2 files changed

+156
-15
lines changed

2 files changed

+156
-15
lines changed

internal/ldconfig/ldconfig.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ 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+
// defaultTopLevelLdsoconfFilePath is the standard location of the top-level ld.so.conf file.
41+
// Most container images based on a distro will have this file, but distroless container images
42+
// may not.
43+
defaultTopLevelLdsoconfFilePath = "/etc/ld.so.conf"
44+
// defaultLdsoconfdDir is the standard location for the ld.so.conf.d drop-in directory. Most
45+
// container images based on a distro will have this directory included by the top-level
46+
// ld.so.conf file, but some may not. And some container images may not have a top-level
47+
// ld.so.conf file at all.
48+
defaultLdsoconfdDir = "/etc/ld.so.conf.d"
4049
)
4150

4251
type Ldconfig struct {
@@ -122,20 +131,22 @@ func (l *Ldconfig) UpdateLDCache() error {
122131

123132
// Explicitly specify using /etc/ld.so.conf since the host's ldconfig may
124133
// be configured to use a different config file by default.
125-
const topLevelLdsoconfFilePath = "/etc/ld.so.conf"
126-
filteredDirectories, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...)
134+
filteredDirectories, err := l.filterDirectories(defaultTopLevelLdsoconfFilePath, l.directories...)
127135
if err != nil {
128136
return err
129137
}
130138

131139
args := []string{
132140
filepath.Base(ldconfigPath),
133-
"-f", topLevelLdsoconfFilePath,
141+
"-f", defaultTopLevelLdsoconfFilePath,
134142
"-C", "/etc/ld.so.cache",
135143
}
136144

137-
if err := createLdsoconfdFile(ldsoconfdFilenamePattern, filteredDirectories...); err != nil {
138-
return fmt.Errorf("failed to update ld.so.conf.d: %w", err)
145+
if err := ensureLdsoconfFile(defaultTopLevelLdsoconfFilePath, defaultLdsoconfdDir); err != nil {
146+
return fmt.Errorf("failed to ensure ld.so.conf file: %w", err)
147+
}
148+
if err := createLdsoconfdFile(defaultLdsoconfdDir, ldsoconfdFilenamePattern, filteredDirectories...); err != nil {
149+
return fmt.Errorf("failed to create ld.so.conf.d drop-in file: %w", err)
139150
}
140151

141152
// In most cases, the hook will be executing a host ldconfig that may be configured widely
@@ -144,8 +155,8 @@ func (l *Ldconfig) UpdateLDCache() error {
144155
// (e.g. /usr/lib/glibc). To avoid all these cases, append the container's expected system
145156
// search paths to the top-level ld.so.conf. This will ensure they get scanned but won't
146157
// 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)
158+
if err := appendSystemSearchPathsToLdsoconf(defaultTopLevelLdsoconfFilePath, l.getSystemSearchPaths()...); err != nil {
159+
return fmt.Errorf("failed to append system search paths to %s: %w", defaultTopLevelLdsoconfFilePath, err)
149160
}
150161

151162
return SafeExec(ldconfigPath, args, nil)
@@ -191,19 +202,15 @@ func (l *Ldconfig) filterDirectories(configFilePath string, directories ...strin
191202
return filtered, nil
192203
}
193204

194-
// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/.
195-
// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and
196-
// contains the specified directories on each line.
197-
func createLdsoconfdFile(pattern string, dirs ...string) error {
205+
// createLdsoconfdFile creates a ld.so.conf.d drop-in file with the specified directories on each
206+
// line. The file is created at `ldsoconfdDir`/{{ .pattern }} using `CreateTemp`.
207+
func createLdsoconfdFile(ldsoconfdDir, pattern string, dirs ...string) error {
198208
if len(dirs) == 0 {
199209
return nil
200210
}
201-
202-
ldsoconfdDir := "/etc/ld.so.conf.d"
203211
if err := os.MkdirAll(ldsoconfdDir, 0755); err != nil {
204212
return fmt.Errorf("failed to create ld.so.conf.d: %w", err)
205213
}
206-
207214
configFile, err := os.CreateTemp(ldsoconfdDir, pattern)
208215
if err != nil {
209216
return fmt.Errorf("failed to create config file: %w", err)
@@ -217,7 +224,7 @@ func createLdsoconfdFile(pattern string, dirs ...string) error {
217224
if added[dir] {
218225
continue
219226
}
220-
_, err = fmt.Fprintf(configFile, "%s\n", dir)
227+
_, err := fmt.Fprintf(configFile, "%s\n", dir)
221228
if err != nil {
222229
return fmt.Errorf("failed to update config file: %w", err)
223230
}
@@ -250,6 +257,19 @@ func appendSystemSearchPathsToLdsoconf(configFilePath string, dirs ...string) er
250257
return nil
251258
}
252259

260+
// ensureLdsoconfFile creates a "standard" top-level ld.so.conf file if none exists.
261+
//
262+
// The created file will contain a single include statement for "`ldsoconfdDir`/*.conf".
263+
func ensureLdsoconfFile(topLevelLdsoconfFilePath, ldsoconfdDir string) error {
264+
configFile, err := os.OpenFile(topLevelLdsoconfFilePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
265+
if err != nil && !os.IsExist(err) {
266+
return fmt.Errorf("failed to create top-level ld.so.conf file: %w", err)
267+
}
268+
defer configFile.Close()
269+
configFile.WriteString("include " + ldsoconfdDir + "/*.conf\n")
270+
return nil
271+
}
272+
253273
// getLdsoconfDirectories returns a map of ldsoconf directories to the conf
254274
// files that refer to the directory.
255275
func (l *Ldconfig) getLdsoconfDirectories(configFilePath string) (map[string]struct{}, error) {

internal/ldconfig/ldconfig_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,124 @@ func TestAppendSystemSearchPathsToLdsoconf(t *testing.T) {
194194
})
195195
}
196196
}
197+
198+
func TestCreateLdsoconfdFile(t *testing.T) {
199+
testCases := []struct {
200+
description string
201+
pattern string
202+
dirs []string
203+
expectedContent []string
204+
}{
205+
{
206+
description: "empty directories",
207+
pattern: "test-*.conf",
208+
dirs: []string{},
209+
expectedContent: nil,
210+
},
211+
{
212+
description: "single directory",
213+
pattern: "test-*.conf",
214+
dirs: []string{"/usr/local/lib"},
215+
expectedContent: []string{
216+
"/usr/local/lib",
217+
},
218+
},
219+
{
220+
description: "multiple directories",
221+
pattern: "test-*.conf",
222+
dirs: []string{"/usr/local/lib", "/opt/lib", "/usr/lib64"},
223+
expectedContent: []string{
224+
"/usr/local/lib",
225+
"/opt/lib",
226+
"/usr/lib64",
227+
},
228+
},
229+
{
230+
description: "duplicate directories",
231+
pattern: "test-*.conf",
232+
dirs: []string{"/usr/local/lib", "/opt/lib", "/usr/local/lib"},
233+
expectedContent: []string{
234+
"/usr/local/lib",
235+
"/opt/lib",
236+
},
237+
},
238+
}
239+
240+
for _, tc := range testCases {
241+
t.Run(tc.description, func(t *testing.T) {
242+
tmpDir := t.TempDir()
243+
244+
err := createLdsoconfdFile(tmpDir, tc.pattern, tc.dirs...)
245+
require.NoError(t, err)
246+
247+
if len(tc.expectedContent) == 0 {
248+
entries, err := os.ReadDir(tmpDir)
249+
require.NoError(t, err)
250+
require.Empty(t, entries)
251+
return
252+
}
253+
254+
entries, err := os.ReadDir(tmpDir)
255+
require.NoError(t, err)
256+
require.Len(t, entries, 1)
257+
createdFile := filepath.Join(tmpDir, entries[0].Name())
258+
259+
info, err := os.Stat(createdFile)
260+
require.NoError(t, err)
261+
require.Equal(t, os.FileMode(0644), info.Mode().Perm())
262+
263+
content, err := os.ReadFile(createdFile)
264+
require.NoError(t, err)
265+
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
266+
require.Equal(t, tc.expectedContent, lines)
267+
})
268+
}
269+
}
270+
271+
func TestEnsureLdsoconfFile(t *testing.T) {
272+
testCases := []struct {
273+
description string
274+
existingContent string
275+
ldsoconfdDir string
276+
expectCreation bool
277+
expectedContent string
278+
}{
279+
{
280+
description: "creates file when none exists",
281+
existingContent: "",
282+
ldsoconfdDir: "/custom/ld.so.conf.d",
283+
expectCreation: true,
284+
expectedContent: "include /custom/ld.so.conf.d/*.conf\n",
285+
},
286+
{
287+
description: "does not modify existing file",
288+
existingContent: "# custom config\n/usr/local/lib\n",
289+
ldsoconfdDir: "/etc/ld.so.conf.d",
290+
expectCreation: false,
291+
expectedContent: "# custom config\n/usr/local/lib\n",
292+
},
293+
}
294+
295+
for _, tc := range testCases {
296+
t.Run(tc.description, func(t *testing.T) {
297+
tmpDir := t.TempDir()
298+
confFilePath := filepath.Join(tmpDir, "ld.so.conf")
299+
300+
if tc.existingContent != "" {
301+
err := os.WriteFile(confFilePath, []byte(tc.existingContent), 0644)
302+
require.NoError(t, err)
303+
}
304+
305+
err := ensureLdsoconfFile(confFilePath, tc.ldsoconfdDir)
306+
require.NoError(t, err)
307+
308+
info, err := os.Stat(confFilePath)
309+
require.NoError(t, err)
310+
require.Equal(t, os.FileMode(0644), info.Mode().Perm())
311+
312+
content, err := os.ReadFile(confFilePath)
313+
require.NoError(t, err)
314+
require.Equal(t, tc.expectedContent, string(content))
315+
})
316+
}
317+
}

0 commit comments

Comments
 (0)