/** # Copyright (c) 2022, 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 ( "errors" "fmt" "log" "os" "strings" "github.com/moby/sys/reexec" "github.com/urfave/cli/v2" "github.com/NVIDIA/nvidia-container-toolkit/internal/config" "github.com/NVIDIA/nvidia-container-toolkit/internal/logger" "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" ) const ( // ldsoconfdFilenamePattern specifies the pattern for the filename // in ld.so.conf.d that includes references to the specified directories. // The 00-nvcr prefix is chosen to ensure that these libraries have a // higher precedence than other libraries on the system, but lower than // the 00-cuda-compat that is included in some containers. ldsoconfdFilenamePattern = "00-nvcr-*.conf" reexecUpdateLdCacheCommandName = "reexec-update-ldcache" ) type command struct { logger logger.Interface } type options struct { folders cli.StringSlice ldconfigPath string containerSpec string } func init() { reexec.Register(reexecUpdateLdCacheCommandName, updateLdCacheHandler) if reexec.Init() { os.Exit(0) } } // NewCommand constructs an update-ldcache command with the specified logger func NewCommand(logger logger.Interface) *cli.Command { c := command{ logger: logger, } return c.build() } // build the update-ldcache command func (m command) build() *cli.Command { cfg := options{} // Create the 'update-ldcache' command c := cli.Command{ Name: "update-ldcache", Usage: "Update ldcache in a container by running ldconfig", 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 add to /etc/ld.so.conf before updating the ld cache", 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) } containerRootDir, err := s.GetContainerRoot() if err != nil || containerRootDir == "" || containerRootDir == "/" { return fmt.Errorf("failed to determined container root: %v", err) } args := []string{ reexecUpdateLdCacheCommandName, strings.TrimPrefix(config.NormalizeLDConfigPath("@"+cfg.ldconfigPath), "@"), containerRootDir, } args = append(args, cfg.folders.Value()...) cmd := createReexecCommand(args) return cmd.Run() } // updateLdCacheHandler wraps updateLdCache with error handling. func updateLdCacheHandler() { if err := updateLdCache(os.Args); err != nil { log.Printf("Error updating ldcache: %v", err) os.Exit(1) } } // updateLdCache is invoked from a reexec'd handler and provides namespace // isolation for the operations performed by this hook. // At the point where this is invoked, we are in a new mount namespace that is // cloned from the parent. // // args[0] is the reexec initializer function name // args[1] is the path of the ldconfig binary on the host // args[2] is the container root directory // The remaining args are folders that need to be added to the ldcache. func updateLdCache(args []string) error { if len(args) < 3 { return fmt.Errorf("incorrect arguments: %v", args) } hostLdconfigPath := args[1] containerRootDirPath := args[2] // To prevent leaking the parent proc filesystem, we create a new proc mount // in the container root. if err := mountProc(containerRootDirPath); err != nil { return fmt.Errorf("error mounting /proc: %w", err) } // We mount the host ldconfig before we pivot root since host paths are not // visible after the pivot root operation. ldconfigPath, err := mountLdConfig(hostLdconfigPath, containerRootDirPath) if err != nil { return fmt.Errorf("error mounting host ldconfig: %w", err) } // We pivot to the container root for the new process, this further limits // access to the host. if err := pivotRoot(containerRootDirPath); err != nil { return fmt.Errorf("error running pivot_root: %w", err) } return runLdconfig(ldconfigPath, args[3:]...) } // runLdconfig runs the ldconfig binary and ensures that the specified directories // are processed for the ldcache. func runLdconfig(ldconfigPath string, directories ...string) error { args := []string{ "ldconfig", // Explicitly specify using /etc/ld.so.conf since the host's ldconfig may // be configured to use a different config file by default. // Note that since we apply the `-r {{ .containerRootDir }}` argument, /etc/ld.so.conf is // in the container. "-f", "/etc/ld.so.conf", } containerRoot := containerRoot("/") if containerRoot.hasPath("/etc/ld.so.cache") { args = append(args, "-C", "/etc/ld.so.cache") } else { args = append(args, "-N") } if containerRoot.hasPath("/etc/ld.so.conf.d") { err := createLdsoconfdFile(ldsoconfdFilenamePattern, directories...) if err != nil { return fmt.Errorf("failed to update ld.so.conf.d: %w", err) } } else { args = append(args, directories...) } return SafeExec(ldconfigPath, args, nil) } // createLdsoconfdFile creates a file at /etc/ld.so.conf.d/. // The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and // contains the specified directories on each line. func createLdsoconfdFile(pattern string, dirs ...string) error { if len(dirs) == 0 { return nil } ldsoconfdDir := "/etc/ld.so.conf.d" 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 func() { _ = configFile.Close() }() added := make(map[string]bool) for _, dir := range dirs { if added[dir] { continue } _, err = fmt.Fprintf(configFile, "%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 }