diff --git a/internal/pathrs/mkdirall.go b/internal/pathrs/mkdirall.go index 81c9a022c..31cc08579 100644 --- a/internal/pathrs/mkdirall.go +++ b/internal/pathrs/mkdirall.go @@ -24,6 +24,14 @@ import ( "path/filepath" ) +func splitPath(path string) (dirPath, filename string, err error) { + dirPath, filename = filepath.Split(path) + if filepath.Join("/", filename) == "/" { + return "", "", fmt.Errorf("root subpath %q has bad trailing component %q", path, filename) + } + return dirPath, filename, nil +} + // MkdirAllParentInRoot is like [MkdirAllInRoot] except that it only creates // the parent directory of the target path, returning the trailing component so // the caller has more flexibility around constructing the final inode. @@ -41,9 +49,9 @@ func MkdirAllParentInRoot(root *os.File, unsafePath string, mode os.FileMode) (* return nil, "", fmt.Errorf("failed to construct hallucinated target path: %w", err) } - dirPath, filename := filepath.Split(unsafePath) - if filepath.Join("/", filename) == "/" { - return nil, "", fmt.Errorf("create parent dir in root subpath %q has bad trailing component %q", unsafePath, filename) + dirPath, filename, err := splitPath(unsafePath) + if err != nil { + return nil, "", fmt.Errorf("split path %q for mkdir parent: %w", unsafePath, err) } dirFd, err := MkdirAllInRoot(root, dirPath, mode) diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go index 0ddabf804..fc5114a85 100644 --- a/internal/pathrs/root_pathrslite.go +++ b/internal/pathrs/root_pathrslite.go @@ -19,7 +19,9 @@ package pathrs import ( + "fmt" "os" + "path/filepath" "github.com/cyphar/filepath-securejoin/pathrs-lite" "golang.org/x/sys/unix" @@ -65,3 +67,46 @@ func CreateInRoot(root *os.File, subpath string, flags int, fileMode uint32) (*o } return os.NewFile(uintptr(fd), root.Name()+"/"+subpath), nil } + +// UnlinkInRoot deletes the inode specified at the given subpath. If you pass +// [unix.AT_REMOVEDIR] it will remove directories, otherwise it will remove +// non-directory inodes. +func UnlinkInRoot(root *os.File, subpath string, flags int) error { + dirPath, filename, err := splitPath(subpath) + if err != nil { + return fmt.Errorf("split path %q for unlink: %w", subpath, err) + } + + dirFd := root + if filepath.Join("/", dirPath) != "/" { + newDirFd, err := OpenInRoot(root, dirPath, unix.O_DIRECTORY|unix.O_PATH) + if err != nil { + return fmt.Errorf("failed to open parent directory %q for unlink: %w", dirPath, err) + } + dirFd = newDirFd + defer dirFd.Close() + } + + err = unix.Unlinkat(int(dirFd.Fd()), filename, flags) + if err != nil { + err = &os.PathError{Op: "unlinkat", Path: dirFd.Name() + "/" + filename, Err: err} + } + return err +} + +// SymlinkInRoot creates a symlink inside a root with the given target (as well +// as creating any missing parent directories). If the subpath already exists, +// an error is returned. +func SymlinkInRoot(linktarget string, root *os.File, subpath string) error { + dirFd, filename, err := MkdirAllParentInRoot(root, subpath, 0o755) + if err != nil { + return err + } + defer dirFd.Close() + + err = unix.Symlinkat(linktarget, int(dirFd.Fd()), filename) + if err != nil { + err = &os.PathError{Op: "symlinkat", Path: dirFd.Name() + "/" + filename, Err: err} + } + return err +} diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 808c3f871..71122dd21 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -97,6 +97,19 @@ func needsSetupDev(config *configs.Config) bool { return true } +func doSetupDev(rootFd *os.File, config *configs.Config) error { + if err := createDevices(rootFd, config); err != nil { + return fmt.Errorf("error creating device nodes: %w", err) + } + if err := setupPtmx(rootFd); err != nil { + return fmt.Errorf("error setting up ptmx: %w", err) + } + if err := setupDevSymlinks(rootFd); err != nil { + return fmt.Errorf("error setting up /dev symlinks: %w", err) + } + return nil +} + // setupAndMountToRootfs sets up the mount for a single mount point and mounts it to the rootfs. func setupAndMountToRootfs(pipe *syncSocket, config *configs.Config, mountConfig *mountConfig, m *configs.Mount) error { entry := mountEntry{Mount: m} @@ -184,14 +197,8 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) { setupDev := needsSetupDev(config) if setupDev { - if err := createDevices(config, rootFd); err != nil { - return fmt.Errorf("error creating device nodes: %w", err) - } - if err := setupPtmx(config); err != nil { - return fmt.Errorf("error setting up ptmx: %w", err) - } - if err := setupDevSymlinks(config.Rootfs); err != nil { - return fmt.Errorf("error setting up /dev symlinks: %w", err) + if err := doSetupDev(rootFd, config); err != nil { + return fmt.Errorf("configuring container /dev: %w", err) } } @@ -380,7 +387,8 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error { // symlink(2) is very dumb, it will just shove the path into // the link and doesn't do any checks or relative path // conversion. Also, don't error out if the cgroup already exists. - if err := os.Symlink(mc, filepath.Join(c.root.Name(), m.Destination, ss)); err != nil && !errors.Is(err, os.ErrExist) { + ssPath := filepath.Join(m.Destination, ss) + if err := pathrs.SymlinkInRoot(mc, c.root, ssPath); err != nil && !errors.Is(err, os.ErrExist) { return err } } @@ -893,7 +901,7 @@ func checkProcMount(rootfs, dest string, m mountEntry) error { return fmt.Errorf("%q cannot be mounted because it is inside /proc", dest) } -func setupDevSymlinks(rootfs string) error { +func setupDevSymlinks(rootFd *os.File) error { // In theory, these should be links to /proc/thread-self, but systems // expect these to be /proc/self and this matches how most distributions // work. @@ -909,11 +917,8 @@ func setupDevSymlinks(rootfs string) error { links = append(links, [2]string{"/proc/kcore", "/dev/core"}) } for _, link := range links { - var ( - src = link[0] - dst = filepath.Join(rootfs, link[1]) - ) - if err := os.Symlink(src, dst); err != nil && !errors.Is(err, os.ErrExist) { + target, devName := link[0], link[1] + if err := pathrs.SymlinkInRoot(target, rootFd, devName); err != nil && !errors.Is(err, os.ErrExist) { return err } } @@ -949,7 +954,7 @@ func reOpenDevNull() error { } // Create the device nodes in the container. -func createDevices(config *configs.Config, rootFd *os.File) error { +func createDevices(rootFd *os.File, config *configs.Config) error { useBindMount := userns.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER) for _, node := range config.Devices { @@ -1129,15 +1134,11 @@ func setReadonly() error { return mount("", "/", "", flags, "") } -func setupPtmx(config *configs.Config) error { - ptmx := filepath.Join(config.Rootfs, "dev/ptmx") - if err := os.Remove(ptmx); err != nil && !errors.Is(err, os.ErrNotExist) { +func setupPtmx(rootFd *os.File) error { + if err := pathrs.UnlinkInRoot(rootFd, "/dev/ptmx", 0); err != nil && !errors.Is(err, os.ErrNotExist) { return err } - if err := os.Symlink("pts/ptmx", ptmx); err != nil { - return err - } - return nil + return pathrs.SymlinkInRoot("pts/ptmx", rootFd, "/dev/ptmx") } // pivotRoot will call pivot_root such that rootfs becomes the new root