diff --git a/cmd/nvidia-cdi-hook/commands/commands.go b/cmd/nvidia-cdi-hook/commands/commands.go index 3f80ba9b..7ad34ca9 100644 --- a/cmd/nvidia-cdi-hook/commands/commands.go +++ b/cmd/nvidia-cdi-hook/commands/commands.go @@ -20,6 +20,7 @@ import ( "github.com/urfave/cli/v2" "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/chmod" + soname "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/create-soname-symlinks" symlinks "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/create-symlinks" "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/cudacompat" ldcache "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/update-ldcache" @@ -34,5 +35,6 @@ func New(logger logger.Interface) []*cli.Command { symlinks.NewCommand(logger), chmod.NewCommand(logger), cudacompat.NewCommand(logger), + soname.NewCommand(logger), } } diff --git a/cmd/nvidia-cdi-hook/create-soname-symlinks/soname-symlinks.go b/cmd/nvidia-cdi-hook/create-soname-symlinks/soname-symlinks.go new file mode 100644 index 00000000..5f163ec7 --- /dev/null +++ b/cmd/nvidia-cdi-hook/create-soname-symlinks/soname-symlinks.go @@ -0,0 +1,131 @@ +/** +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package soname + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/urfave/cli/v2" + + "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/utils" + "github.com/NVIDIA/nvidia-container-toolkit/internal/logger" + "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" +) + +type command struct { + logger logger.Interface + utils.SafeExecer +} + +type options struct { + folders cli.StringSlice + ldconfigPath string + containerSpec string +} + +// NewCommand constructs an create-soname-symlinks command with the specified logger +func NewCommand(logger logger.Interface) *cli.Command { + c := command{ + logger: logger, + SafeExecer: utils.NewSafeExecer(logger), + } + return c.build() +} + +// build the create-soname-symlinks command +func (m command) build() *cli.Command { + cfg := options{} + + // Create the 'create-soname-symlinks' command + c := cli.Command{ + Name: "create-soname-symlinks", + Usage: "Create soname symlinks for the specified folders using ldconfig -n -N", + Before: func(c *cli.Context) error { + return m.validateFlags(c, &cfg) + }, + Action: func(c *cli.Context) error { + return m.run(c, &cfg) + }, + } + + c.Flags = []cli.Flag{ + &cli.StringSliceFlag{ + Name: "folder", + Usage: "Specify a folder to search for shared libraries for which soname symlinks need to be created", + Destination: &cfg.folders, + }, + &cli.StringFlag{ + Name: "ldconfig-path", + Usage: "Specify the path to the ldconfig program", + Destination: &cfg.ldconfigPath, + Value: "/sbin/ldconfig", + }, + &cli.StringFlag{ + Name: "container-spec", + Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN", + Destination: &cfg.containerSpec, + }, + } + + return &c +} + +func (m command) validateFlags(c *cli.Context, cfg *options) error { + if cfg.ldconfigPath == "" { + return errors.New("ldconfig-path must be specified") + } + return nil +} + +func (m command) run(c *cli.Context, cfg *options) error { + s, err := oci.LoadContainerState(cfg.containerSpec) + if err != nil { + return fmt.Errorf("failed to load container state: %v", err) + } + + containerRoot, err := s.GetContainerRoot() + if err != nil { + return fmt.Errorf("failed to determined container root: %v", err) + } + if containerRoot == "" { + m.logger.Warningf("No container root detected") + return nil + } + + dirs := cfg.folders.Value() + if len(dirs) == 0 { + return nil + } + + ldconfigPath := utils.ResolveHostLDConfigPath(cfg.ldconfigPath) + args := []string{filepath.Base(ldconfigPath)} + + args = append(args, + // Specify the containerRoot to use. + "-r", containerRoot, + // Specify -n to only process the specified folders. + "-n", + // Explicitly disable updating the LDCache. + "-N", + ) + // Explicitly specific the directories to add. + args = append(args, dirs...) + + return m.Exec(ldconfigPath, args, nil) +} diff --git a/cmd/nvidia-cdi-hook/cudacompat/container-root.go b/cmd/nvidia-cdi-hook/cudacompat/container-root.go deleted file mode 100644 index 8bb3b3c8..00000000 --- a/cmd/nvidia-cdi-hook/cudacompat/container-root.go +++ /dev/null @@ -1,76 +0,0 @@ -/** -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -**/ - -package cudacompat - -import ( - "os" - "path/filepath" - - "github.com/moby/sys/symlink" -) - -// A containerRoot represents the root filesystem of a container. -type containerRoot string - -// hasPath checks whether the specified path exists in the root. -func (r containerRoot) hasPath(path string) bool { - resolved, err := r.resolve(path) - if err != nil { - return false - } - if _, err := os.Stat(resolved); err != nil && os.IsNotExist(err) { - return false - } - return true -} - -// globFiles matches the specified pattern in the root. -// The files that match must be regular files. -func (r containerRoot) globFiles(pattern string) ([]string, error) { - patternPath, err := r.resolve(pattern) - if err != nil { - return nil, err - } - matches, err := filepath.Glob(patternPath) - if err != nil { - return nil, err - } - var files []string - for _, match := range matches { - info, err := os.Lstat(match) - if err != nil { - return nil, err - } - // Ignore symlinks. - if info.Mode()&os.ModeSymlink != 0 { - continue - } - // Ignore directories. - if info.IsDir() { - continue - } - files = append(files, match) - } - return files, nil -} - -// resolve returns the absolute path including root path. -// Symlinks are resolved, but are guaranteed to resolve in the root. -func (r containerRoot) resolve(path string) (string, error) { - absolute := filepath.Clean(filepath.Join(string(r), path)) - return symlink.FollowSymlinkInScope(absolute, string(r)) -} diff --git a/cmd/nvidia-cdi-hook/cudacompat/cudacompat.go b/cmd/nvidia-cdi-hook/cudacompat/cudacompat.go index 0cecd6c1..83cbf7b8 100644 --- a/cmd/nvidia-cdi-hook/cudacompat/cudacompat.go +++ b/cmd/nvidia-cdi-hook/cudacompat/cudacompat.go @@ -18,13 +18,13 @@ package cudacompat import ( "fmt" - "os" "path/filepath" "strconv" "strings" "github.com/urfave/cli/v2" + "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/utils" "github.com/NVIDIA/nvidia-container-toolkit/internal/logger" "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" ) @@ -107,8 +107,9 @@ func (m command) run(_ *cli.Context, cfg *options) error { if err != nil { return fmt.Errorf("failed to determined container root: %w", err) } + containerRoot := utils.ContainerRoot(containerRootDir) - containerForwardCompatDir, err := m.getContainerForwardCompatDir(containerRoot(containerRootDir), cfg.hostDriverVersion) + containerForwardCompatDir, err := m.getContainerForwardCompatDir(containerRoot, cfg.hostDriverVersion) if err != nil { return fmt.Errorf("failed to get container forward compat directory: %w", err) } @@ -116,24 +117,24 @@ func (m command) run(_ *cli.Context, cfg *options) error { return nil } - return m.createLdsoconfdFile(containerRoot(containerRootDir), cudaCompatLdsoconfdFilenamePattern, containerForwardCompatDir) + return containerRoot.CreateLdsoconfdFile(cudaCompatLdsoconfdFilenamePattern, containerForwardCompatDir) } -func (m command) getContainerForwardCompatDir(containerRoot containerRoot, hostDriverVersion string) (string, error) { +func (m command) getContainerForwardCompatDir(containerRoot utils.ContainerRoot, hostDriverVersion string) (string, error) { if hostDriverVersion == "" { m.logger.Debugf("Host driver version not specified") return "", nil } - if !containerRoot.hasPath(cudaCompatPath) { + if !containerRoot.HasPath(cudaCompatPath) { m.logger.Debugf("No CUDA forward compatibility libraries directory in container") return "", nil } - if !containerRoot.hasPath("/etc/ld.so.cache") { + if !containerRoot.HasPath("/etc/ld.so.cache") { m.logger.Debugf("The container does not have an LDCache") return "", nil } - libs, err := containerRoot.globFiles(filepath.Join(cudaCompatPath, "libcuda.so.*.*")) + libs, err := containerRoot.GlobFiles(filepath.Join(cudaCompatPath, "libcuda.so.*.*")) if err != nil { m.logger.Warningf("Failed to find CUDA compat library: %w", err) return "", nil @@ -169,51 +170,6 @@ func (m command) getContainerForwardCompatDir(containerRoot containerRoot, hostD return resolvedCompatDir, nil } -// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/ in the specified root. -// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and -// contains the specified directories on each line. -func (m command) createLdsoconfdFile(in containerRoot, pattern string, dirs ...string) error { - if len(dirs) == 0 { - m.logger.Debugf("No directories to add to /etc/ld.so.conf") - return nil - } - - ldsoconfdDir, err := in.resolve("/etc/ld.so.conf.d") - if err != nil { - return err - } - if err := os.MkdirAll(ldsoconfdDir, 0755); err != nil { - return fmt.Errorf("failed to create ld.so.conf.d: %w", err) - } - - configFile, err := os.CreateTemp(ldsoconfdDir, pattern) - if err != nil { - return fmt.Errorf("failed to create config file: %w", err) - } - defer configFile.Close() - - m.logger.Debugf("Adding directories %v to %v", dirs, configFile.Name()) - - added := make(map[string]bool) - for _, dir := range dirs { - if added[dir] { - continue - } - _, err = configFile.WriteString(fmt.Sprintf("%s\n", dir)) - if err != nil { - return fmt.Errorf("failed to update config file: %w", err) - } - added[dir] = true - } - - // The created file needs to be world readable for the cases where the container is run as a non-root user. - if err := configFile.Chmod(0644); err != nil { - return fmt.Errorf("failed to chmod config file: %w", err) - } - - return nil -} - // extractMajorVersion parses a version string and returns the major version as an int. func extractMajorVersion(version string) (int, error) { majorString := strings.SplitN(version, ".", 2)[0] diff --git a/cmd/nvidia-cdi-hook/cudacompat/cudacompat_test.go b/cmd/nvidia-cdi-hook/cudacompat/cudacompat_test.go index 0422fe76..f2d4d770 100644 --- a/cmd/nvidia-cdi-hook/cudacompat/cudacompat_test.go +++ b/cmd/nvidia-cdi-hook/cudacompat/cudacompat_test.go @@ -24,6 +24,8 @@ import ( testlog "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/require" + + "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/utils" ) func TestCompatLibs(t *testing.T) { @@ -130,53 +132,9 @@ func TestCompatLibs(t *testing.T) { c := command{ logger: logger, } - containerForwardCompatDir, err := c.getContainerForwardCompatDir(containerRoot(containerRootDir), tc.hostDriverVersion) + containerForwardCompatDir, err := c.getContainerForwardCompatDir(utils.ContainerRoot(containerRootDir), tc.hostDriverVersion) require.NoError(t, err) require.EqualValues(t, tc.expectedContainerForwardCompatDir, containerForwardCompatDir) }) } } - -func TestUpdateLdconfig(t *testing.T) { - logger, _ := testlog.NewNullLogger() - testCases := []struct { - description string - folders []string - expectedContents string - }{ - { - description: "no folders; have no contents", - }, - { - description: "single folder is added", - folders: []string{"/usr/local/cuda/compat"}, - expectedContents: "/usr/local/cuda/compat\n", - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - containerRootDir := t.TempDir() - c := command{ - logger: logger, - } - err := c.createLdsoconfdFile(containerRoot(containerRootDir), cudaCompatLdsoconfdFilenamePattern, tc.folders...) - require.NoError(t, err) - - matches, err := filepath.Glob(filepath.Join(containerRootDir, "/etc/ld.so.conf.d/00-compat-*.conf")) - require.NoError(t, err) - - if tc.expectedContents == "" { - require.Empty(t, matches) - return - } - - require.Len(t, matches, 1) - contents, err := os.ReadFile(matches[0]) - require.NoError(t, err) - - require.EqualValues(t, tc.expectedContents, string(contents)) - }) - } - -} diff --git a/cmd/nvidia-cdi-hook/update-ldcache/container-root.go b/cmd/nvidia-cdi-hook/update-ldcache/container-root.go deleted file mode 100644 index 71a49469..00000000 --- a/cmd/nvidia-cdi-hook/update-ldcache/container-root.go +++ /dev/null @@ -1,46 +0,0 @@ -/** -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -**/ - -package ldcache - -import ( - "os" - "path/filepath" - - "github.com/moby/sys/symlink" -) - -// A containerRoot represents the root filesystem of a container. -type containerRoot string - -// hasPath checks whether the specified path exists in the root. -func (r containerRoot) hasPath(path string) bool { - resolved, err := r.resolve(path) - if err != nil { - return false - } - if _, err := os.Stat(resolved); err != nil && os.IsNotExist(err) { - return false - } - return true -} - -// resolve returns the absolute path including root path. -// Symlinks are resolved, but are guaranteed to resolve in the root. -func (r containerRoot) resolve(path string) (string, error) { - absolute := filepath.Clean(filepath.Join(string(r), path)) - return symlink.FollowSymlinkInScope(absolute, string(r)) -} diff --git a/cmd/nvidia-cdi-hook/update-ldcache/update-ldcache.go b/cmd/nvidia-cdi-hook/update-ldcache/update-ldcache.go index 18cf4e1c..4344b51c 100644 --- a/cmd/nvidia-cdi-hook/update-ldcache/update-ldcache.go +++ b/cmd/nvidia-cdi-hook/update-ldcache/update-ldcache.go @@ -19,13 +19,11 @@ package ldcache import ( "errors" "fmt" - "os" "path/filepath" - "strings" "github.com/urfave/cli/v2" - "github.com/NVIDIA/nvidia-container-toolkit/internal/config" + "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/utils" "github.com/NVIDIA/nvidia-container-toolkit/internal/logger" "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" ) @@ -40,6 +38,7 @@ const ( ) type command struct { + utils.SafeExecer logger logger.Interface } @@ -52,7 +51,8 @@ type options struct { // NewCommand constructs an update-ldcache command with the specified logger func NewCommand(logger logger.Interface) *cli.Command { c := command{ - logger: logger, + logger: logger, + SafeExecer: utils.NewSafeExecer(logger), } return c.build() } @@ -113,15 +113,15 @@ func (m command) run(c *cli.Context, cfg *options) error { return fmt.Errorf("failed to determined container root: %v", err) } - ldconfigPath := m.resolveLDConfigPath(cfg.ldconfigPath) + ldconfigPath := utils.ResolveHostLDConfigPath(cfg.ldconfigPath) args := []string{filepath.Base(ldconfigPath)} if containerRootDir != "" { args = append(args, "-r", containerRootDir) } - containerRoot := containerRoot(containerRootDir) + containerRoot := utils.ContainerRoot(containerRootDir) - if containerRoot.hasPath("/etc/ld.so.cache") { + if containerRoot.HasPath("/etc/ld.so.cache") { args = append(args, "-C", "/etc/ld.so.cache") } else { m.logger.Debugf("No ld.so.cache found, skipping update") @@ -129,8 +129,8 @@ func (m command) run(c *cli.Context, cfg *options) error { } folders := cfg.folders.Value() - if containerRoot.hasPath("/etc/ld.so.conf.d") { - err := m.createLdsoconfdFile(containerRoot, ldsoconfdFilenamePattern, folders...) + if containerRoot.HasPath("/etc/ld.so.conf.d") { + err := containerRoot.CreateLdsoconfdFile(ldsoconfdFilenamePattern, folders...) if err != nil { return fmt.Errorf("failed to update ld.so.conf.d: %v", err) } @@ -142,57 +142,5 @@ func (m command) run(c *cli.Context, cfg *options) error { // be configured to use a different config file by default. args = append(args, "-f", "/etc/ld.so.conf") - return m.SafeExec(ldconfigPath, args, nil) -} - -// resolveLDConfigPath determines the LDConfig path to use for the system. -// On systems such as Ubuntu where `/sbin/ldconfig` is a wrapper around -// /sbin/ldconfig.real, the latter is returned. -func (m command) resolveLDConfigPath(path string) string { - return strings.TrimPrefix(config.NormalizeLDConfigPath("@"+path), "@") -} - -// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/ in the specified root. -// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and -// contains the specified directories on each line. -func (m command) createLdsoconfdFile(in containerRoot, pattern string, dirs ...string) error { - if len(dirs) == 0 { - m.logger.Debugf("No directories to add to /etc/ld.so.conf") - return nil - } - - ldsoconfdDir, err := in.resolve("/etc/ld.so.conf.d") - if err != nil { - return err - } - if err := os.MkdirAll(ldsoconfdDir, 0755); err != nil { - return fmt.Errorf("failed to create ld.so.conf.d: %w", err) - } - - configFile, err := os.CreateTemp(ldsoconfdDir, pattern) - if err != nil { - return fmt.Errorf("failed to create config file: %w", err) - } - defer configFile.Close() - - m.logger.Debugf("Adding directories %v to %v", dirs, configFile.Name()) - - added := make(map[string]bool) - for _, dir := range dirs { - if added[dir] { - continue - } - _, err = configFile.WriteString(fmt.Sprintf("%s\n", dir)) - if err != nil { - return fmt.Errorf("failed to update config file: %w", err) - } - added[dir] = true - } - - // The created file needs to be world readable for the cases where the container is run as a non-root user. - if err := configFile.Chmod(0644); err != nil { - return fmt.Errorf("failed to chmod config file: %w", err) - } - - return nil + return m.Exec(ldconfigPath, args, nil) } diff --git a/cmd/nvidia-cdi-hook/utils/container-root.go b/cmd/nvidia-cdi-hook/utils/container-root.go new file mode 100644 index 00000000..3f339141 --- /dev/null +++ b/cmd/nvidia-cdi-hook/utils/container-root.go @@ -0,0 +1,119 @@ +/** +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package utils + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/moby/sys/symlink" +) + +// A ContainerRoot represents the root filesystem of a container. +type ContainerRoot string + +// CreateLdsoconfdFile creates a file at /etc/ld.so.conf.d/ in the specified container root. +// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and +// contains the specified directories on each line. +func (r ContainerRoot) CreateLdsoconfdFile(pattern string, dirs ...string) error { + if len(dirs) == 0 { + return nil + } + + ldsoconfdDir, err := r.Resolve("/etc/ld.so.conf.d") + if err != nil { + return err + } + if err := os.MkdirAll(ldsoconfdDir, 0755); err != nil { + return fmt.Errorf("failed to create ld.so.conf.d: %w", err) + } + + configFile, err := os.CreateTemp(ldsoconfdDir, pattern) + if err != nil { + return fmt.Errorf("failed to create config file: %w", err) + } + defer configFile.Close() + + added := make(map[string]bool) + for _, dir := range dirs { + if added[dir] { + continue + } + _, err = configFile.WriteString(fmt.Sprintf("%s\n", dir)) + if err != nil { + return fmt.Errorf("failed to update config file: %w", err) + } + added[dir] = true + } + + // The created file needs to be world readable for the cases where the container is run as a non-root user. + if err := configFile.Chmod(0644); err != nil { + return fmt.Errorf("failed to chmod config file: %w", err) + } + + return nil +} + +// GlobFiles matches the specified pattern in the container root. +// The files that match must be regular files. +func (r ContainerRoot) GlobFiles(pattern string) ([]string, error) { + patternPath, err := r.Resolve(pattern) + if err != nil { + return nil, err + } + matches, err := filepath.Glob(patternPath) + if err != nil { + return nil, err + } + var files []string + for _, match := range matches { + info, err := os.Lstat(match) + if err != nil { + return nil, err + } + // Ignore symlinks. + if info.Mode()&os.ModeSymlink != 0 { + continue + } + // Ignore directories. + if info.IsDir() { + continue + } + files = append(files, match) + } + return files, nil +} + +// HasPath checks whether the specified path exists in the root. +func (r ContainerRoot) HasPath(path string) bool { + resolved, err := r.Resolve(path) + if err != nil { + return false + } + if _, err := os.Stat(resolved); err != nil && os.IsNotExist(err) { + return false + } + return true +} + +// Resolve returns the absolute path including root path. +// Symlinks are resolved, but are guaranteed to resolve in the root. +func (r ContainerRoot) Resolve(path string) (string, error) { + absolute := filepath.Clean(filepath.Join(string(r), path)) + return symlink.FollowSymlinkInScope(absolute, string(r)) +} diff --git a/cmd/nvidia-cdi-hook/utils/container-root_test.go b/cmd/nvidia-cdi-hook/utils/container-root_test.go new file mode 100644 index 00000000..b4ff73bb --- /dev/null +++ b/cmd/nvidia-cdi-hook/utils/container-root_test.go @@ -0,0 +1,66 @@ +/** +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package utils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUpdateLdconfig(t *testing.T) { + testCases := []struct { + description string + folders []string + expectedContents string + }{ + { + description: "no folders; have no contents", + }, + { + description: "single folder is added", + folders: []string{"/usr/local/cuda/compat"}, + expectedContents: "/usr/local/cuda/compat\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + containerRootDir := t.TempDir() + c := ContainerRoot(containerRootDir) + err := c.CreateLdsoconfdFile("00-compat-*.conf", tc.folders...) + require.NoError(t, err) + + matches, err := filepath.Glob(filepath.Join(containerRootDir, "/etc/ld.so.conf.d/00-compat-*.conf")) + require.NoError(t, err) + + if tc.expectedContents == "" { + require.Empty(t, matches) + return + } + + require.Len(t, matches, 1) + contents, err := os.ReadFile(matches[0]) + require.NoError(t, err) + + require.EqualValues(t, tc.expectedContents, string(contents)) + }) + } + +} diff --git a/cmd/nvidia-cdi-hook/utils/ldconfig.go b/cmd/nvidia-cdi-hook/utils/ldconfig.go new file mode 100644 index 00000000..58922138 --- /dev/null +++ b/cmd/nvidia-cdi-hook/utils/ldconfig.go @@ -0,0 +1,30 @@ +/** +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package utils + +import ( + "strings" + + "github.com/NVIDIA/nvidia-container-toolkit/internal/config" +) + +// ResolveHostLDConfigPath determines the LDConfig path to use for the system. +// On systems such as Ubuntu where `/sbin/ldconfig` is a wrapper around +// /sbin/ldconfig.real, the latter is returned. +func ResolveHostLDConfigPath(path string) string { + return strings.TrimPrefix(config.NormalizeLDConfigPath("@"+path), "@") +} diff --git a/cmd/nvidia-cdi-hook/utils/safe-exec.go b/cmd/nvidia-cdi-hook/utils/safe-exec.go new file mode 100644 index 00000000..b15440de --- /dev/null +++ b/cmd/nvidia-cdi-hook/utils/safe-exec.go @@ -0,0 +1,32 @@ +/** +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package utils + +import "github.com/NVIDIA/nvidia-container-toolkit/internal/logger" + +// A SafeExecer is used to Exec an application from a memfd to prevent possible +// tampering. +type SafeExecer struct { + logger logger.Interface +} + +// NewSafeExecer creates a SafeExecer with the specified logger. +func NewSafeExecer(logger logger.Interface) SafeExecer { + return SafeExecer{ + logger: logger, + } +} diff --git a/cmd/nvidia-cdi-hook/update-ldcache/safe-exec_linux.go b/cmd/nvidia-cdi-hook/utils/safe-exec_linux.go similarity index 85% rename from cmd/nvidia-cdi-hook/update-ldcache/safe-exec_linux.go rename to cmd/nvidia-cdi-hook/utils/safe-exec_linux.go index c1c655b4..30556c5b 100644 --- a/cmd/nvidia-cdi-hook/update-ldcache/safe-exec_linux.go +++ b/cmd/nvidia-cdi-hook/utils/safe-exec_linux.go @@ -14,7 +14,7 @@ # limitations under the License. **/ -package ldcache +package utils import ( "fmt" @@ -25,11 +25,11 @@ import ( "github.com/opencontainers/runc/libcontainer/dmz" ) -// SafeExec attempts to clone the specified binary (as an memfd, for example) before executing it. -func (m command) SafeExec(path string, args []string, envv []string) error { +// Exec attempts to clone the specified binary (as an memfd, for example) before executing it. +func (s SafeExecer) Exec(path string, args []string, envv []string) error { safeExe, err := cloneBinary(path) if err != nil { - m.logger.Warningf("Failed to clone binary %q: %v; falling back to Exec", path, err) + s.logger.Warningf("Failed to clone binary %q: %v; falling back to Exec", path, err) //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection return syscall.Exec(path, args, envv) } diff --git a/cmd/nvidia-cdi-hook/update-ldcache/safe-exec_other.go b/cmd/nvidia-cdi-hook/utils/safe-exec_other.go similarity index 75% rename from cmd/nvidia-cdi-hook/update-ldcache/safe-exec_other.go rename to cmd/nvidia-cdi-hook/utils/safe-exec_other.go index dff11dd3..9534dcfd 100644 --- a/cmd/nvidia-cdi-hook/update-ldcache/safe-exec_other.go +++ b/cmd/nvidia-cdi-hook/utils/safe-exec_other.go @@ -17,13 +17,14 @@ # limitations under the License. **/ -package ldcache +package utils import "syscall" -// SafeExec is not implemented on non-linux systems and forwards directly to the +// Exec is not implemented on non-linux systems and forwards directly to the // Exec syscall. -func (m *command) SafeExec(path string, args []string, envv []string) error { +func (s SafeExecer) Exec(path string, args []string, envv []string) error { + s.logger.Warningf("Cloning binary not implemented for binary %q; falling back to Exec", path) //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection return syscall.Exec(path, args, envv) } diff --git a/cmd/nvidia-ctk-installer/container/toolkit/toolkit_test.go b/cmd/nvidia-ctk-installer/container/toolkit/toolkit_test.go index 4aad36c5..c763d701 100644 --- a/cmd/nvidia-ctk-installer/container/toolkit/toolkit_test.go +++ b/cmd/nvidia-ctk-installer/container/toolkit/toolkit_test.go @@ -98,6 +98,13 @@ containerEdits: - hookName: createContainer path: {{ .toolkitRoot }}/nvidia-cdi-hook args: + - nvidia-cdi-hook + - create-soname-symlinks + - --folder + - /lib/x86_64-linux-gnu + hookName: createContainer + path: {{ .toolkitRoot }}/nvidia-cdi-hook + - args: - nvidia-cdi-hook - update-ldcache - --folder diff --git a/cmd/nvidia-ctk/cdi/generate/generate_test.go b/cmd/nvidia-ctk/cdi/generate/generate_test.go index c74cff23..004c2104 100644 --- a/cmd/nvidia-ctk/cdi/generate/generate_test.go +++ b/cmd/nvidia-ctk/cdi/generate/generate_test.go @@ -100,6 +100,13 @@ containerEdits: - hookName: createContainer path: /usr/bin/nvidia-cdi-hook args: + - nvidia-cdi-hook + - create-soname-symlinks + - --folder + - /lib/x86_64-linux-gnu + hookName: createContainer + path: /usr/bin/nvidia-cdi-hook + - args: - nvidia-cdi-hook - update-ldcache - --folder diff --git a/internal/discover/ldconfig.go b/internal/discover/ldconfig.go index b81b9be5..db0526ec 100644 --- a/internal/discover/ldconfig.go +++ b/internal/discover/ldconfig.go @@ -50,16 +50,16 @@ func (d ldconfig) Hooks() ([]Hook, error) { if err != nil { return nil, fmt.Errorf("failed to discover mounts for ldcache update: %v", err) } - h := CreateLDCacheUpdateHook( + hooks := CreateLDCacheUpdateHooks( d.nvidiaCDIHookPath, d.ldconfigPath, getLibraryPaths(mounts), ) - return []Hook{h}, nil + return hooks, nil } -// CreateLDCacheUpdateHook locates the NVIDIA Container Toolkit CLI and creates a hook for updating the LD Cache -func CreateLDCacheUpdateHook(executable string, ldconfig string, libraries []string) Hook { +// CreateLDCacheUpdateHooks locates the NVIDIA Container Toolkit CLI and creates a hook for updating the LD Cache +func CreateLDCacheUpdateHooks(executable string, ldconfig string, libraries []string) []Hook { var args []string if ldconfig != "" { @@ -70,13 +70,20 @@ func CreateLDCacheUpdateHook(executable string, ldconfig string, libraries []str args = append(args, "--folder", f) } - hook := CreateNvidiaCDIHook( - executable, - "update-ldcache", - args..., - ) + hooks := []Hook{ + CreateNvidiaCDIHook( + executable, + "create-soname-symlinks", + args..., + ), + CreateNvidiaCDIHook( + executable, + "update-ldcache", + args..., + ), + } - return hook + return hooks } // getLibraryPaths extracts the library dirs from the specified mounts diff --git a/internal/discover/ldconfig_test.go b/internal/discover/ldconfig_test.go index 0b214c77..2c2da46c 100644 --- a/internal/discover/ldconfig_test.go +++ b/internal/discover/ldconfig_test.go @@ -38,11 +38,22 @@ func TestLDCacheUpdateHook(t *testing.T) { mounts []Mount mountError error expectedError error - expectedArgs []string + expectedHooks []Hook }{ { - description: "empty mounts", - expectedArgs: []string{"nvidia-cdi-hook", "update-ldcache"}, + description: "empty mounts", + expectedHooks: []Hook{ + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "create-soname-symlinks"}, + }, + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "update-ldcache"}, + }, + }, }, { description: "mount error", @@ -65,7 +76,18 @@ func TestLDCacheUpdateHook(t *testing.T) { Path: "/usr/local/lib/libbar.so", }, }, - expectedArgs: []string{"nvidia-cdi-hook", "update-ldcache", "--folder", "/usr/local/lib", "--folder", "/usr/local/libother"}, + expectedHooks: []Hook{ + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "create-soname-symlinks", "--folder", "/usr/local/lib", "--folder", "/usr/local/libother"}, + }, + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "update-ldcache", "--folder", "/usr/local/lib", "--folder", "/usr/local/libother"}, + }, + }, }, { description: "host paths are ignored", @@ -75,12 +97,34 @@ func TestLDCacheUpdateHook(t *testing.T) { Path: "/usr/local/lib/libfoo.so", }, }, - expectedArgs: []string{"nvidia-cdi-hook", "update-ldcache", "--folder", "/usr/local/lib"}, + expectedHooks: []Hook{ + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "create-soname-symlinks", "--folder", "/usr/local/lib"}, + }, + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "update-ldcache", "--folder", "/usr/local/lib"}, + }, + }, }, { description: "explicit ldconfig path is passed", ldconfigPath: testLdconfigPath, - expectedArgs: []string{"nvidia-cdi-hook", "update-ldcache", "--ldconfig-path", testLdconfigPath}, + expectedHooks: []Hook{ + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "create-soname-symlinks", "--ldconfig-path", testLdconfigPath}, + }, + { + Lifecycle: "createContainer", + Path: testNvidiaCDIHookPath, + Args: []string{"nvidia-cdi-hook", "update-ldcache", "--ldconfig-path", testLdconfigPath}, + }, + }, }, } @@ -91,12 +135,6 @@ func TestLDCacheUpdateHook(t *testing.T) { return tc.mounts, tc.mountError }, } - expectedHook := Hook{ - Path: testNvidiaCDIHookPath, - Args: tc.expectedArgs, - Lifecycle: "createContainer", - } - d, err := NewLDCacheUpdateHook(logger, mountMock, testNvidiaCDIHookPath, tc.ldconfigPath) require.NoError(t, err) @@ -110,9 +148,7 @@ func TestLDCacheUpdateHook(t *testing.T) { } require.NoError(t, err) - require.Len(t, hooks, 1) - - require.EqualValues(t, hooks[0], expectedHook) + require.EqualValues(t, tc.expectedHooks, hooks) devices, err := d.Devices() require.NoError(t, err) diff --git a/tests/e2e/nvidia-container-toolkit_test.go b/tests/e2e/nvidia-container-toolkit_test.go index 5948014b..cde77be7 100644 --- a/tests/e2e/nvidia-container-toolkit_test.go +++ b/tests/e2e/nvidia-container-toolkit_test.go @@ -215,4 +215,26 @@ var _ = Describe("docker", Ordered, ContinueOnFailure, func() { Expect(ldconfigOut).To(ContainSubstring("/usr/lib64")) }) }) + + When("A container is run using CDI", Ordered, func() { + BeforeAll(func(ctx context.Context) { + _, _, err := r.Run("docker pull ubuntu") + Expect(err).ToNot(HaveOccurred()) + }) + + It("should include libcuda.so in the ldcache", func(ctx context.Context) { + ldcacheOutput, _, err := r.Run("docker run --rm -i --runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=runtime.nvidia.com/gpu=all ubuntu bash -c \"ldconfig -p | grep 'libcuda.so'\"") + Expect(err).ToNot(HaveOccurred()) + Expect(ldcacheOutput).ToNot(BeEmpty()) + + ldcacheLines := strings.Split(ldcacheOutput, "\n") + var libs []string + for _, line := range ldcacheLines { + parts := strings.SplitN(line, " (", 2) + libs = append(libs, strings.TrimSpace(parts[0])) + } + + Expect(libs).To(ContainElements([]string{"libcuda.so", "libcuda.so.1"})) + }) + }) })