From 50aa47ba69aba8f48ddc3e7a621f4181246cff1b Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 9 Nov 2025 01:40:57 +1100 Subject: [PATCH 1/6] libcontainer: move CleanPath and StripRoot to internal/pathrs These helpers will be needed for the compatibility code added in future patches in this series, but because "internal/pathrs" is imported by "libcontainer/utils" we need to move them so that we can avoid circular dependencies. Because the old functions were in a non-internal package it is possible some downstreams use them, so add some wrappers but mark them as deprecated. Signed-off-by: Aleksa Sarai (cherry picked from commit 42a1e19d6788fde798fa960f047afbffbc319f8e) Signed-off-by: Aleksa Sarai --- internal/pathrs/path.go | 50 +++++++++++++++++ internal/pathrs/path_test.go | 67 +++++++++++++++++++++++ libcontainer/apparmor/apparmor_linux.go | 3 +- libcontainer/factory_linux.go | 4 +- libcontainer/rootfs_linux.go | 14 ++--- libcontainer/specconv/spec_linux.go | 12 ++--- libcontainer/utils/utils.go | 71 ++++++++----------------- libcontainer/utils/utils_test.go | 67 ----------------------- libcontainer/utils/utils_unix.go | 7 +-- 9 files changed, 159 insertions(+), 136 deletions(-) diff --git a/internal/pathrs/path.go b/internal/pathrs/path.go index 1ee7c795d5b..709c3ae3f9d 100644 --- a/internal/pathrs/path.go +++ b/internal/pathrs/path.go @@ -19,6 +19,8 @@ package pathrs import ( + "os" + "path/filepath" "strings" ) @@ -32,3 +34,51 @@ func IsLexicallyInRoot(root, path string) bool { path = strings.TrimRight(path, "/") return strings.HasPrefix(path+"/", root+"/") } + +// LexicallyCleanPath makes a path safe for use with filepath.Join. This is +// done by not only cleaning the path, but also (if the path is relative) +// adding a leading '/' and cleaning it (then removing the leading '/'). This +// ensures that a path resulting from prepending another path will always +// resolve to lexically be a subdirectory of the prefixed path. This is all +// done lexically, so paths that include symlinks won't be safe as a result of +// using CleanPath. +func LexicallyCleanPath(path string) string { + // Deal with empty strings nicely. + if path == "" { + return "" + } + + // Ensure that all paths are cleaned (especially problematic ones like + // "/../../../../../" which can cause lots of issues). + + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + + // If the path isn't absolute, we need to do more processing to fix paths + // such as "../../../..//some/path". We also shouldn't convert absolute + // paths to relative ones. + path = filepath.Clean(string(os.PathSeparator) + path) + // This can't fail, as (by definition) all paths are relative to root. + path, _ = filepath.Rel(string(os.PathSeparator), path) + + return path +} + +// LexicallyStripRoot returns the passed path, stripping the root path if it +// was (lexicially) inside it. Note that both passed paths will always be +// treated as absolute, and the returned path will also always be absolute. In +// addition, the paths are cleaned before stripping the root. +func LexicallyStripRoot(root, path string) string { + // Make the paths clean and absolute. + root, path = LexicallyCleanPath("/"+root), LexicallyCleanPath("/"+path) + switch { + case path == root: + path = "/" + case root == "/": + // do nothing + default: + path = strings.TrimPrefix(path, root+"/") + } + return LexicallyCleanPath("/" + path) +} diff --git a/internal/pathrs/path_test.go b/internal/pathrs/path_test.go index 19d577fba3b..b7fdde6b94a 100644 --- a/internal/pathrs/path_test.go +++ b/internal/pathrs/path_test.go @@ -51,3 +51,70 @@ func TestIsLexicallyInRoot(t *testing.T) { }) } } + +func TestLexicallyCleanPath(t *testing.T) { + path := LexicallyCleanPath("") + if path != "" { + t.Errorf("expected to receive empty string and received %s", path) + } + + path = LexicallyCleanPath("rootfs") + if path != "rootfs" { + t.Errorf("expected to receive 'rootfs' and received %s", path) + } + + path = LexicallyCleanPath("../../../var") + if path != "var" { + t.Errorf("expected to receive 'var' and received %s", path) + } + + path = LexicallyCleanPath("/../../../var") + if path != "/var" { + t.Errorf("expected to receive '/var' and received %s", path) + } + + path = LexicallyCleanPath("/foo/bar/") + if path != "/foo/bar" { + t.Errorf("expected to receive '/foo/bar' and received %s", path) + } + + path = LexicallyCleanPath("/foo/bar/../") + if path != "/foo" { + t.Errorf("expected to receive '/foo' and received %s", path) + } +} + +func TestLexicallyStripRoot(t *testing.T) { + for _, test := range []struct { + root, path, out string + }{ + // Works with multiple components. + {"/a/b", "/a/b/c", "/c"}, + {"/hello/world", "/hello/world/the/quick-brown/fox", "/the/quick-brown/fox"}, + // '/' must be a no-op. + {"/", "/a/b/c", "/a/b/c"}, + // Must be the correct order. + {"/a/b", "/a/c/b", "/a/c/b"}, + // Must be at start. + {"/abc/def", "/foo/abc/def/bar", "/foo/abc/def/bar"}, + // Must be a lexical parent. + {"/foo/bar", "/foo/barSAMECOMPONENT", "/foo/barSAMECOMPONENT"}, + // Must only strip the root once. + {"/foo/bar", "/foo/bar/foo/bar/baz", "/foo/bar/baz"}, + // Deal with .. in a fairly sane way. + {"/foo/bar", "/foo/bar/../baz", "/foo/baz"}, + {"/foo/bar", "../../../../../../foo/bar/baz", "/baz"}, + {"/foo/bar", "/../../../../../../foo/bar/baz", "/baz"}, + {"/foo/bar/../baz", "/foo/baz/bar", "/bar"}, + {"/foo/bar/../baz", "/foo/baz/../bar/../baz/./foo", "/foo"}, + // All paths are made absolute before stripping. + {"foo/bar", "/foo/bar/baz/bee", "/baz/bee"}, + {"/foo/bar", "foo/bar/baz/beef", "/baz/beef"}, + {"foo/bar", "foo/bar/baz/beets", "/baz/beets"}, + } { + got := LexicallyStripRoot(test.root, test.path) + if got != test.out { + t.Errorf("LexicallyStripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out) + } + } +} diff --git a/libcontainer/apparmor/apparmor_linux.go b/libcontainer/apparmor/apparmor_linux.go index 2cde88bae7d..9bb4fb2cd2d 100644 --- a/libcontainer/apparmor/apparmor_linux.go +++ b/libcontainer/apparmor/apparmor_linux.go @@ -9,7 +9,6 @@ import ( "golang.org/x/sys/unix" "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer/utils" ) var ( @@ -29,7 +28,7 @@ func isEnabled() bool { } func setProcAttr(attr, value string) error { - attr = utils.CleanPath(attr) + attr = pathrs.LexicallyCleanPath(attr) attrSubPath := "attr/apparmor/" + attr if _, err := os.Stat("/proc/self/" + attrSubPath); errors.Is(err, os.ErrNotExist) { // fall back to the old convention diff --git a/libcontainer/factory_linux.go b/libcontainer/factory_linux.go index b799e4a98a9..70d935c0d38 100644 --- a/libcontainer/factory_linux.go +++ b/libcontainer/factory_linux.go @@ -11,10 +11,10 @@ import ( "github.com/opencontainers/cgroups" "github.com/opencontainers/cgroups/manager" + "github.com/opencontainers/runc/internal/pathrs" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/configs/validate" "github.com/opencontainers/runc/libcontainer/intelrdt" - "github.com/opencontainers/runc/libcontainer/utils" ) const ( @@ -211,7 +211,7 @@ func validateID(id string) error { } - if string(os.PathSeparator)+id != utils.CleanPath(string(os.PathSeparator)+id) { + if string(os.PathSeparator)+id != pathrs.LexicallyCleanPath(string(os.PathSeparator)+id) { return ErrInvalidID } diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 9f2af0a212d..f6f58987f07 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -91,7 +91,7 @@ func (m mountEntry) srcStatfs() (*unix.Statfs_t, error) { // needsSetupDev returns true if /dev needs to be set up. func needsSetupDev(config *configs.Config) bool { for _, m := range config.Mounts { - if m.Device == "bind" && utils.CleanPath(m.Destination) == "/dev" { + if m.Device == "bind" && pathrs.LexicallyCleanPath(m.Destination) == "/dev" { return false } } @@ -257,7 +257,7 @@ func finalizeRootfs(config *configs.Config) (err error) { if m.Flags&unix.MS_RDONLY != unix.MS_RDONLY { continue } - if m.Device == "tmpfs" || utils.CleanPath(m.Destination) == "/dev" { + if m.Device == "tmpfs" || pathrs.LexicallyCleanPath(m.Destination) == "/dev" { if err := remountReadonly(m); err != nil { return err } @@ -506,7 +506,7 @@ func statfsToMountFlags(st unix.Statfs_t) int { var errRootfsToFile = errors.New("config tries to change rootfs to file") func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { - unsafePath := utils.StripRoot(rootfs, m.Destination) + unsafePath := pathrs.LexicallyStripRoot(rootfs, m.Destination) dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH) defer func() { if dstFile != nil && Err != nil { @@ -553,7 +553,7 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { if err != nil { return err } - unsafePath = utils.StripRoot(rootfs, newUnsafePath) + unsafePath = pathrs.LexicallyStripRoot(rootfs, newUnsafePath) if dstIsFile { dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) @@ -952,7 +952,7 @@ func createDevices(config *configs.Config) error { for _, node := range config.Devices { // The /dev/ptmx device is setup by setupPtmx() - if utils.CleanPath(node.Path) == "/dev/ptmx" { + if pathrs.LexicallyCleanPath(node.Path) == "/dev/ptmx" { continue } @@ -1392,7 +1392,7 @@ func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err err if !pathrs.IsLexicallyInRoot(rootfs, fullPath) { return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs) } - unsafePath := utils.StripRoot(rootfs, fullPath) + unsafePath := pathrs.LexicallyStripRoot(rootfs, fullPath) reopened, err := pathrs.OpenInRoot(rootfs, unsafePath, flags) if err != nil { return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err) @@ -1432,7 +1432,7 @@ func (m *mountEntry) mountPropagate(rootfs string, mountLabel string) error { // operations on it. We need to set up files in "/dev", and other tmpfs // mounts may need to be chmod-ed after mounting. These mounts will be // remounted ro later in finalizeRootfs(), if necessary. - if m.Device == "tmpfs" || utils.CleanPath(m.Destination) == "/dev" { + if m.Device == "tmpfs" || pathrs.LexicallyCleanPath(m.Destination) == "/dev" { flags &= ^unix.MS_RDONLY } diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index ae2dd5d2715..214197c3db5 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -16,17 +16,17 @@ import ( systemdDbus "github.com/coreos/go-systemd/v22/dbus" dbus "github.com/godbus/dbus/v5" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + "github.com/opencontainers/cgroups" devices "github.com/opencontainers/cgroups/devices/config" "github.com/opencontainers/runc/internal/linux" + "github.com/opencontainers/runc/internal/pathrs" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/internal/userns" "github.com/opencontainers/runc/libcontainer/seccomp" - libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils" - "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" - - "golang.org/x/sys/unix" ) var ( @@ -802,7 +802,7 @@ func CreateCgroupConfig(opts *CreateOpts, defaultDevs []*devices.Device) (*cgrou if useSystemdCgroup { myCgroupPath = spec.Linux.CgroupsPath } else { - myCgroupPath = libcontainerUtils.CleanPath(spec.Linux.CgroupsPath) + myCgroupPath = pathrs.LexicallyCleanPath(spec.Linux.CgroupsPath) } } diff --git a/libcontainer/utils/utils.go b/libcontainer/utils/utils.go index e4a9013a721..6c3baf0153c 100644 --- a/libcontainer/utils/utils.go +++ b/libcontainer/utils/utils.go @@ -3,11 +3,11 @@ package utils import ( "encoding/json" "io" - "os" - "path/filepath" "strings" "golang.org/x/sys/unix" + + "github.com/opencontainers/runc/internal/pathrs" ) const ( @@ -36,53 +36,6 @@ func WriteJSON(w io.Writer, v any) error { return err } -// CleanPath makes a path safe for use with filepath.Join. This is done by not -// only cleaning the path, but also (if the path is relative) adding a leading -// '/' and cleaning it (then removing the leading '/'). This ensures that a -// path resulting from prepending another path will always resolve to lexically -// be a subdirectory of the prefixed path. This is all done lexically, so paths -// that include symlinks won't be safe as a result of using CleanPath. -func CleanPath(path string) string { - // Deal with empty strings nicely. - if path == "" { - return "" - } - - // Ensure that all paths are cleaned (especially problematic ones like - // "/../../../../../" which can cause lots of issues). - - if filepath.IsAbs(path) { - return filepath.Clean(path) - } - - // If the path isn't absolute, we need to do more processing to fix paths - // such as "../../../..//some/path". We also shouldn't convert absolute - // paths to relative ones. - path = filepath.Clean(string(os.PathSeparator) + path) - // This can't fail, as (by definition) all paths are relative to root. - path, _ = filepath.Rel(string(os.PathSeparator), path) - - return path -} - -// StripRoot returns the passed path, stripping the root path if it was -// (lexicially) inside it. Note that both passed paths will always be treated -// as absolute, and the returned path will also always be absolute. In -// addition, the paths are cleaned before stripping the root. -func StripRoot(root, path string) string { - // Make the paths clean and absolute. - root, path = CleanPath("/"+root), CleanPath("/"+path) - switch { - case path == root: - path = "/" - case root == "/": - // do nothing - default: - path = strings.TrimPrefix(path, root+"/") - } - return CleanPath("/" + path) -} - // SearchLabels searches through a list of key=value pairs for a given key, // returning its value, and the binary flag telling whether the key exist. func SearchLabels(labels []string, key string) (string, bool) { @@ -113,3 +66,23 @@ func Annotations(labels []string) (bundle string, userAnnotations map[string]str } return bundle, userAnnotations } + +// CleanPath makes a path safe for use with filepath.Join. This is done by not +// only cleaning the path, but also (if the path is relative) adding a leading +// '/' and cleaning it (then removing the leading '/'). This ensures that a +// path resulting from prepending another path will always resolve to lexically +// be a subdirectory of the prefixed path. This is all done lexically, so paths +// that include symlinks won't be safe as a result of using CleanPath. +// +// Deprecated: This function has been moved to internal/pathrs and this wrapper +// will be removed in runc 1.5. +var CleanPath = pathrs.LexicallyCleanPath + +// StripRoot returns the passed path, stripping the root path if it was +// (lexicially) inside it. Note that both passed paths will always be treated +// as absolute, and the returned path will also always be absolute. In +// addition, the paths are cleaned before stripping the root. +// +// Deprecated: This function has been moved to internal/pathrs and this wrapper +// will be removed in runc 1.5. +var StripRoot = pathrs.LexicallyStripRoot diff --git a/libcontainer/utils/utils_test.go b/libcontainer/utils/utils_test.go index 4b5fd833cdf..5953118efcc 100644 --- a/libcontainer/utils/utils_test.go +++ b/libcontainer/utils/utils_test.go @@ -70,70 +70,3 @@ func TestWriteJSON(t *testing.T) { t.Errorf("expected to write %s but was %s", expected, b.String()) } } - -func TestCleanPath(t *testing.T) { - path := CleanPath("") - if path != "" { - t.Errorf("expected to receive empty string and received %s", path) - } - - path = CleanPath("rootfs") - if path != "rootfs" { - t.Errorf("expected to receive 'rootfs' and received %s", path) - } - - path = CleanPath("../../../var") - if path != "var" { - t.Errorf("expected to receive 'var' and received %s", path) - } - - path = CleanPath("/../../../var") - if path != "/var" { - t.Errorf("expected to receive '/var' and received %s", path) - } - - path = CleanPath("/foo/bar/") - if path != "/foo/bar" { - t.Errorf("expected to receive '/foo/bar' and received %s", path) - } - - path = CleanPath("/foo/bar/../") - if path != "/foo" { - t.Errorf("expected to receive '/foo' and received %s", path) - } -} - -func TestStripRoot(t *testing.T) { - for _, test := range []struct { - root, path, out string - }{ - // Works with multiple components. - {"/a/b", "/a/b/c", "/c"}, - {"/hello/world", "/hello/world/the/quick-brown/fox", "/the/quick-brown/fox"}, - // '/' must be a no-op. - {"/", "/a/b/c", "/a/b/c"}, - // Must be the correct order. - {"/a/b", "/a/c/b", "/a/c/b"}, - // Must be at start. - {"/abc/def", "/foo/abc/def/bar", "/foo/abc/def/bar"}, - // Must be a lexical parent. - {"/foo/bar", "/foo/barSAMECOMPONENT", "/foo/barSAMECOMPONENT"}, - // Must only strip the root once. - {"/foo/bar", "/foo/bar/foo/bar/baz", "/foo/bar/baz"}, - // Deal with .. in a fairly sane way. - {"/foo/bar", "/foo/bar/../baz", "/foo/baz"}, - {"/foo/bar", "../../../../../../foo/bar/baz", "/baz"}, - {"/foo/bar", "/../../../../../../foo/bar/baz", "/baz"}, - {"/foo/bar/../baz", "/foo/baz/bar", "/bar"}, - {"/foo/bar/../baz", "/foo/baz/../bar/../baz/./foo", "/foo"}, - // All paths are made absolute before stripping. - {"foo/bar", "/foo/bar/baz/bee", "/baz/bee"}, - {"/foo/bar", "foo/bar/baz/beef", "/baz/beef"}, - {"foo/bar", "foo/bar/baz/beets", "/baz/beets"}, - } { - got := StripRoot(test.root, test.path) - if got != test.out { - t.Errorf("StripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out) - } - } -} diff --git a/libcontainer/utils/utils_unix.go b/libcontainer/utils/utils_unix.go index a457a09b00e..37ec5993960 100644 --- a/libcontainer/utils/utils_unix.go +++ b/libcontainer/utils/utils_unix.go @@ -13,10 +13,11 @@ import ( _ "unsafe" // for go:linkname securejoin "github.com/cyphar/filepath-securejoin" - "github.com/opencontainers/runc/internal/linux" - "github.com/opencontainers/runc/internal/pathrs" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" + + "github.com/opencontainers/runc/internal/linux" + "github.com/opencontainers/runc/internal/pathrs" ) var ( @@ -153,7 +154,7 @@ func NewSockPair(name string) (parent, child *os.File, err error) { // the passed closure (the file handle will be freed once the closure returns). func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { // Remove the root then forcefully resolve inside the root. - unsafePath = StripRoot(root, unsafePath) + unsafePath = pathrs.LexicallyStripRoot(root, unsafePath) fullPath, err := securejoin.SecureJoin(root, unsafePath) if err != nil { return fmt.Errorf("resolving path inside rootfs failed: %w", err) From c2bde92ce8df5714dc1b8efdc6dd1d6cc40e52cd Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 9 Nov 2025 02:03:42 +1100 Subject: [PATCH 2/6] libct: switch final WithProcfd users to WithProcfdFile This probably should've been done as part of commit d40b3439a961 ("rootfs: switch to fd-based handling of mountpoint targets") but it seems I missed them when doing the rest of the conversions. This also lets us remove utils.WithProcfd entirely, as well as pathrs.MkdirAllInRoot. Unfortunately, WithProcfd was exposed in the externally-importable "libcontainer/utils" package and so we need to have a deprecation notice to remove it in runc 1.5. Signed-off-by: Aleksa Sarai (cherry picked from commit 9dbd37e06f8124be6e496308e1735d18f27bb730) Signed-off-by: Aleksa Sarai --- internal/pathrs/mkdirall_pathrslite.go | 10 ---------- libcontainer/criu_linux.go | 15 +++++++++++---- libcontainer/rootfs_linux.go | 11 ++++++----- libcontainer/utils/utils_unix.go | 12 ++++++++++-- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/internal/pathrs/mkdirall_pathrslite.go b/internal/pathrs/mkdirall_pathrslite.go index a9a0157c681..3fd476fbfc2 100644 --- a/internal/pathrs/mkdirall_pathrslite.go +++ b/internal/pathrs/mkdirall_pathrslite.go @@ -87,13 +87,3 @@ func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, er return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) }) } - -// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the -// returned handle, for callers that don't need to use it. -func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error { - f, err := MkdirAllInRootOpen(root, unsafePath, mode) - if err == nil { - _ = f.Close() - } - return err -} diff --git a/libcontainer/criu_linux.go b/libcontainer/criu_linux.go index f6f5c906fd6..54dfdfd9cc1 100644 --- a/libcontainer/criu_linux.go +++ b/libcontainer/criu_linux.go @@ -24,6 +24,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/opencontainers/cgroups" + "github.com/opencontainers/runc/internal/pathrs" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/utils" ) @@ -542,16 +543,22 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error { umounts := []string{} defer func() { for _, u := range umounts { - _ = utils.WithProcfd(c.config.Rootfs, u, func(procfd string) error { - if e := unix.Unmount(procfd, unix.MNT_DETACH); e != nil { - if e != unix.EINVAL { + mntFile, err := pathrs.OpenInRoot(c.config.Rootfs, u, unix.O_PATH) + if err != nil { + logrus.Warnf("Error during cleanup unmounting %s: open handle: %v", u, err) + continue + } + _ = utils.WithProcfdFile(mntFile, func(procfd string) error { + if err := unix.Unmount(procfd, unix.MNT_DETACH); err != nil { + if err != unix.EINVAL { // Ignore EINVAL as it means 'target is not a mount point.' // It probably has already been unmounted. - logrus.Warnf("Error during cleanup unmounting of %s (%s): %v", procfd, u, e) + logrus.Warnf("Error during cleanup unmounting of %s (%s): %v", procfd, u, err) } } return nil }) + _ = mntFile.Close() } }() // Now go through all mounts and create the required mountpoints. diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index f6f58987f07..05a7807e63d 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -329,12 +329,15 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error { // We just created the tmpfs, and so we can just use filepath.Join // here (not to mention we want to make sure we create the path // inside the tmpfs, so we don't want to resolve symlinks). + // TODO: Why not just use b.Destination (c.root is the root here)? subsystemPath := filepath.Join(c.root, b.Destination) subsystemName := filepath.Base(b.Destination) - if err := pathrs.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil { + subsystemDir, err := pathrs.MkdirAllInRootOpen(c.root, subsystemPath, 0o755) + if err != nil { return err } - if err := utils.WithProcfd(c.root, b.Destination, func(dstFd string) error { + defer subsystemDir.Close() + if err := utils.WithProcfdFile(subsystemDir, func(dstFd string) error { flags := defaultMountFlags if m.Flags&unix.MS_RDONLY != 0 { flags = flags | unix.MS_RDONLY @@ -1456,9 +1459,7 @@ func (m *mountEntry) mountPropagate(rootfs string, mountLabel string) error { _ = m.dstFile.Close() m.dstFile = newDstFile - // We have to apply mount propagation flags in a separate WithProcfd() call - // because the previous call invalidates the passed procfd -- the mount - // target needs to be re-opened. + // Apply the propagation flags on the new mount. if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error { for _, pflag := range m.PropagationFlags { if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil { diff --git a/libcontainer/utils/utils_unix.go b/libcontainer/utils/utils_unix.go index 37ec5993960..af5689e9a2d 100644 --- a/libcontainer/utils/utils_unix.go +++ b/libcontainer/utils/utils_unix.go @@ -152,6 +152,9 @@ func NewSockPair(name string) (parent, child *os.File, err error) { // through the passed fdpath should be safe. Do not access this path through // the original path strings, and do not attempt to use the pathname outside of // the passed closure (the file handle will be freed once the closure returns). +// +// Deprecated: This function is an internal implementation detail of runc and +// is no longer used. It will be removed in runc 1.5. func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { // Remove the root then forcefully resolve inside the root. unsafePath = pathrs.LexicallyStripRoot(root, unsafePath) @@ -181,10 +184,15 @@ func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { return fn(procfd) } -// WithProcfdFile is a very minimal wrapper around [ProcThreadSelfFd], intended -// to make migrating from [WithProcfd] and [WithProcfdPath] usage easier. The +// WithProcfdFile is a very minimal wrapper around [ProcThreadSelfFd]. The // caller is responsible for making sure that the provided file handle is // actually safe to operate on. +// +// NOTE: THIS FUNCTION IS INTERNAL TO RUNC, DO NOT USE IT. +// +// TODO: Migrate the mount logic towards a more move_mount(2)-friendly design +// where this is kind of /proc/self/... tomfoolery is only done in a fallback +// path for old kernels. func WithProcfdFile(file *os.File, fn func(procfd string) error) error { fdpath, closer := ProcThreadSelfFd(file.Fd()) defer closer() From 7bf92b9d6a0a3edded1ca9bf5c399844cef110ae Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 9 Nov 2025 02:26:57 +1100 Subject: [PATCH 3/6] pathrs: rename MkdirAllInRootOpen -> MkdirAllInRoot Now that MkdirAllInRoot has been removed, we can make MkdirAllInRootOpen less wordy by renaming it to MkdirAllInRoot. This is a non-functional change. Signed-off-by: Aleksa Sarai (cherry picked from commit 20c5a8ec4a16b4ad8d10f3b9748f5ba66698bb18) Signed-off-by: Aleksa Sarai --- internal/pathrs/mkdirall_pathrslite.go | 6 +++--- internal/pathrs/root_pathrslite.go | 2 +- libcontainer/rootfs_linux.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/pathrs/mkdirall_pathrslite.go b/internal/pathrs/mkdirall_pathrslite.go index 3fd476fbfc2..0661273b135 100644 --- a/internal/pathrs/mkdirall_pathrslite.go +++ b/internal/pathrs/mkdirall_pathrslite.go @@ -28,7 +28,7 @@ import ( "golang.org/x/sys/unix" ) -// MkdirAllInRootOpen attempts to make +// MkdirAllInRoot attempts to make // // path, _ := securejoin.SecureJoin(root, unsafePath) // os.MkdirAll(path, mode) @@ -49,10 +49,10 @@ import ( // handling if unsafePath has already been scoped within the rootfs (this is // needed for a lot of runc callers and fixing this would require reworking a // lot of path logic). -func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, error) { +func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) { // If the path is already "within" the root, get the path relative to the // root and use that as the unsafe path. This is necessary because a lot of - // MkdirAllInRootOpen callers have already done SecureJoin, and refactoring + // MkdirAllInRoot callers have already done SecureJoin, and refactoring // all of them to stop using these SecureJoin'd paths would require a fair // amount of work. // TODO(cyphar): Do the refactor to libpathrs once it's ready. diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go index 9e69e8d6c54..4e310ff6d54 100644 --- a/internal/pathrs/root_pathrslite.go +++ b/internal/pathrs/root_pathrslite.go @@ -55,7 +55,7 @@ func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, e return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename) } - dirFd, err := MkdirAllInRootOpen(root, dir, 0o755) + dirFd, err := MkdirAllInRoot(root, dir, 0o755) if err != nil { return nil, err } diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 05a7807e63d..5b46a1efddf 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -332,7 +332,7 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error { // TODO: Why not just use b.Destination (c.root is the root here)? subsystemPath := filepath.Join(c.root, b.Destination) subsystemName := filepath.Base(b.Destination) - subsystemDir, err := pathrs.MkdirAllInRootOpen(c.root, subsystemPath, 0o755) + subsystemDir, err := pathrs.MkdirAllInRoot(c.root, subsystemPath, 0o755) if err != nil { return err } @@ -561,7 +561,7 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { if dstIsFile { dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) } else { - dstFile, err = pathrs.MkdirAllInRootOpen(rootfs, unsafePath, 0o755) + dstFile, err = pathrs.MkdirAllInRoot(rootfs, unsafePath, 0o755) } if err != nil { return fmt.Errorf("make mountpoint %q: %w", m.Destination, err) @@ -623,7 +623,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { } else if !fi.IsDir() { return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device) } - dstFile, err := pathrs.MkdirAllInRootOpen(rootfs, dest, 0o755) + dstFile, err := pathrs.MkdirAllInRoot(rootfs, dest, 0o755) if err != nil { return err } @@ -994,7 +994,7 @@ func createDeviceNode(rootfs string, node *devices.Device, bind bool) error { return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile) } destDirPath, destName := filepath.Split(destPath) - destDir, err := pathrs.MkdirAllInRootOpen(rootfs, destDirPath, 0o755) + destDir, err := pathrs.MkdirAllInRoot(rootfs, destDirPath, 0o755) if err != nil { return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err) } From da73ade8daa75a0e213b50951eb79ee79b155d6b Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 9 Nov 2025 01:41:10 +1100 Subject: [PATCH 4/6] pathrs: add "hallucination" helpers for SecureJoin magic In order to maintain compatibility with previous releases of runc (which permitted dangling symlinks as path components by permitting non-existent path components to be treated like real directories) we have to first do SecureJoin to construct a target path that is compatible with the old behaviour but has all dangling symlinks (or other invalid paths like ".." components after non-existent directories) removed. This is effectively a more generic verison of commit 3f925525b44d ("rootfs: re-allow dangling symlinks in mount targets") and will let us remove the need for open-coding SecureJoin workarounds. Signed-off-by: Aleksa Sarai (cherry picked from commit cfb74326be5865b13bd91c4374b2fb6a70838c59) Signed-off-by: Aleksa Sarai --- internal/pathrs/mkdirall_pathrslite.go | 16 +++---------- internal/pathrs/path.go | 32 ++++++++++++++++++++++++++ internal/pathrs/root_pathrslite.go | 5 ++++ libcontainer/rootfs_linux.go | 12 ---------- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/internal/pathrs/mkdirall_pathrslite.go b/internal/pathrs/mkdirall_pathrslite.go index 0661273b135..c2578e051fe 100644 --- a/internal/pathrs/mkdirall_pathrslite.go +++ b/internal/pathrs/mkdirall_pathrslite.go @@ -21,7 +21,6 @@ package pathrs import ( "fmt" "os" - "path/filepath" "github.com/cyphar/filepath-securejoin/pathrs-lite" "github.com/sirupsen/logrus" @@ -50,18 +49,9 @@ import ( // needed for a lot of runc callers and fixing this would require reworking a // lot of path logic). func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) { - // If the path is already "within" the root, get the path relative to the - // root and use that as the unsafe path. This is necessary because a lot of - // MkdirAllInRoot callers have already done SecureJoin, and refactoring - // all of them to stop using these SecureJoin'd paths would require a fair - // amount of work. - // TODO(cyphar): Do the refactor to libpathrs once it's ready. - if IsLexicallyInRoot(root, unsafePath) { - subPath, err := filepath.Rel(root, unsafePath) - if err != nil { - return nil, err - } - unsafePath = subPath + unsafePath, err := hallucinateUnsafePath(root, unsafePath) + if err != nil { + return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err) } // Check for any silly mode bits. diff --git a/internal/pathrs/path.go b/internal/pathrs/path.go index 709c3ae3f9d..77be9892411 100644 --- a/internal/pathrs/path.go +++ b/internal/pathrs/path.go @@ -22,6 +22,8 @@ import ( "os" "path/filepath" "strings" + + securejoin "github.com/cyphar/filepath-securejoin" ) // IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"), @@ -82,3 +84,33 @@ func LexicallyStripRoot(root, path string) string { } return LexicallyCleanPath("/" + path) } + +// hallucinateUnsafePath creates a new unsafePath which has all symlinks +// (including dangling symlinks) fully resolved and any non-existent components +// treated as though they are real. This is effectively just a wrapper around +// [securejoin.SecureJoin] that strips the root. This path *IS NOT* safe to use +// as-is, you *MUST* operate on the returned path with pathrs-lite. +// +// The reason for this methods is that in previous runc versions, we would +// tolerate nonsense paths with dangling symlinks as path components. +// pathrs-lite does not support this, so instead we have to emulate this +// behaviour by doing SecureJoin *purely to get a semi-reasonable path to use* +// and then we use pathrs-lite to operate on the path safely. +// +// It would be quite difficult to emulate this in a race-free way in +// pathrs-lite, so instead we use [securejoin.SecureJoin] to simply produce a +// new candidate path for operations like [MkdirAllInRoot] so they can then +// operate on the new unsafePath as if it was what the user requested. +// +// If unsafePath is already lexically inside root, it is stripped before +// re-resolving it (this is done to ensure compatibility with legacy callers +// within runc that call SecureJoin before calling into pathrs). +func hallucinateUnsafePath(root, unsafePath string) (string, error) { + unsafePath = LexicallyStripRoot(root, unsafePath) + weirdPath, err := securejoin.SecureJoin(root, unsafePath) + if err != nil { + return "", err + } + unsafePath = LexicallyStripRoot(root, weirdPath) + return unsafePath, nil +} diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go index 4e310ff6d54..0d91484193c 100644 --- a/internal/pathrs/root_pathrslite.go +++ b/internal/pathrs/root_pathrslite.go @@ -50,6 +50,11 @@ func OpenInRoot(root, subpath string, flags int) (*os.File, error) { // include it in the passed flags. The fileMode argument uses unix.* mode bits, // *not* os.FileMode. func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) { + subpath, err := hallucinateUnsafePath(root, subpath) + if err != nil { + return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err) + } + dir, filename := filepath.Split(subpath) if filepath.Join("/", filename) == "/" { return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename) diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 5b46a1efddf..16eccd256aa 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -546,18 +546,6 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { } dstIsFile = !fi.IsDir() } - - // In previous runc versions, we would tolerate nonsense paths with - // dangling symlinks as path components. pathrs-lite does not support - // this, so instead we have to emulate this behaviour by doing - // SecureJoin *purely to get a semi-reasonable path to use* and then we - // use pathrs-lite to operate on the path safely. - newUnsafePath, err := securejoin.SecureJoin(rootfs, unsafePath) - if err != nil { - return err - } - unsafePath = pathrs.LexicallyStripRoot(rootfs, newUnsafePath) - if dstIsFile { dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) } else { From aa3be89f0d2303b62a9185e4451cd9cf38198447 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 9 Nov 2025 04:36:49 +1100 Subject: [PATCH 5/6] pathrs: add MkdirAllParentInRoot helper While CreateInRoot supports hallucinating the target path, we do not use it directly when constructing device inode targets because we need to have different handling for mknod and bind-mounts. The solution is to simply have a more generic MkdirAllParentInRoot helper that MkdirAll's the parent directory of the target path and then allows the caller to create the trailing component however they like. (This can be used by CreateInRoot internally as well!) Signed-off-by: Aleksa Sarai (cherry picked from commit 195e9551e4200b9f6c59d8ee51ccd1fc92ae137e) Signed-off-by: Aleksa Sarai --- internal/pathrs/mkdirall.go | 51 ++++++++++++++++++++++++++++++ internal/pathrs/root_pathrslite.go | 14 +------- libcontainer/rootfs_linux.go | 13 +------- 3 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 internal/pathrs/mkdirall.go diff --git a/internal/pathrs/mkdirall.go b/internal/pathrs/mkdirall.go new file mode 100644 index 00000000000..3a896f48415 --- /dev/null +++ b/internal/pathrs/mkdirall.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2024-2025 Aleksa Sarai + * Copyright (C) 2024-2025 SUSE LLC + * + * 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 pathrs + +import ( + "fmt" + "os" + "path/filepath" +) + +// 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. +// +// Callers need to be very careful operating on the trailing path, as trivial +// mistakes like following symlinks can cause security bugs. Most people +// should probably just use [MkdirAllInRoot] or [CreateInRoot]. +func MkdirAllParentInRoot(root, unsafePath string, mode os.FileMode) (*os.File, string, error) { + // MkdirAllInRoot also does hallucinateUnsafePath, but we need to do it + // here first because when we split unsafePath into (dir, file) components + // we want to be doing so with the hallucinated path (so that trailing + // dangling symlinks are treated correctly). + unsafePath, err := hallucinateUnsafePath(root, unsafePath) + if err != nil { + 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) + } + + dirFd, err := MkdirAllInRoot(root, dirPath, mode) + return dirFd, filename, err +} diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go index 0d91484193c..51db77440d7 100644 --- a/internal/pathrs/root_pathrslite.go +++ b/internal/pathrs/root_pathrslite.go @@ -19,9 +19,7 @@ package pathrs import ( - "fmt" "os" - "path/filepath" "github.com/cyphar/filepath-securejoin/pathrs-lite" "golang.org/x/sys/unix" @@ -50,17 +48,7 @@ func OpenInRoot(root, subpath string, flags int) (*os.File, error) { // include it in the passed flags. The fileMode argument uses unix.* mode bits, // *not* os.FileMode. func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) { - subpath, err := hallucinateUnsafePath(root, subpath) - if err != nil { - return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err) - } - - dir, filename := filepath.Split(subpath) - if filepath.Join("/", filename) == "/" { - return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename) - } - - dirFd, err := MkdirAllInRoot(root, dir, 0o755) + dirFd, filename, err := MkdirAllParentInRoot(root, subpath, 0o755) if err != nil { return nil, err } diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 16eccd256aa..040ef3f6067 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -12,7 +12,6 @@ import ( "syscall" "time" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" "github.com/moby/sys/mountinfo" "github.com/moby/sys/userns" @@ -506,8 +505,6 @@ func statfsToMountFlags(st unix.Statfs_t) int { return flags } -var errRootfsToFile = errors.New("config tries to change rootfs to file") - func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { unsafePath := pathrs.LexicallyStripRoot(rootfs, m.Destination) dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH) @@ -974,15 +971,7 @@ func createDeviceNode(rootfs string, node *devices.Device, bind bool) error { // The node only exists for cgroup reasons, ignore it here. return nil } - destPath, err := securejoin.SecureJoin(rootfs, node.Path) - if err != nil { - return err - } - if destPath == rootfs { - return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile) - } - destDirPath, destName := filepath.Split(destPath) - destDir, err := pathrs.MkdirAllInRoot(rootfs, destDirPath, 0o755) + destDir, destName, err := pathrs.MkdirAllParentInRoot(rootfs, node.Path, 0o755) if err != nil { return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err) } From 6a270e49a36aec28bfc4225fe57f48c7a147480c Mon Sep 17 00:00:00 2001 From: lifubang Date: Mon, 17 Nov 2025 15:23:23 +0000 Subject: [PATCH 6/6] integration: add some tests for bind mount through dangling symlinks We intentionally broke this in commit d40b3439a961 ("rootfs: switch to fd-based handling of mountpoint targets") under the assumption that most users do not need this feature. Sadly it turns out they do, and so commit 3f925525b44d ("rootfs: re-allow dangling symlinks in mount targets") added a hotfix to re-add this functionality. This patch adds some much-needed tests for this behaviour, since it seems we are going to need to keep this for compatibility reasons (at least until runc v2...). Co-developed-by: lifubang Signed-off-by: Aleksa Sarai (cherry picked from commit 15d7c214cde94884c3dc875974d7cbc231a726f0) Signed-off-by: Aleksa Sarai --- tests/integration/mounts.bats | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/integration/mounts.bats b/tests/integration/mounts.bats index b60c88ae27c..6bab15727a3 100644 --- a/tests/integration/mounts.bats +++ b/tests/integration/mounts.bats @@ -127,6 +127,42 @@ function test_mount_order() { [[ "$output" == *"a/x"* ]] # the final "file" was from a/x. } +# This needs to be placed at the top of the bats file to work around +# a shellcheck bug. See . +test_mount_target() { + src="$1" + dst="$2" + real_dst="${3:-$dst}" + + echo "== $src -> $dst (=> $real_dst) ==" + + old_config="$(mktemp ./config.json.bak.XXXXXX)" + cp ./config.json "$old_config" + + update_config '.mounts += [{ + source: "'"$src"'", + destination: "'"$dst"'", + options: ["bind"] + }]' + + # Make sure the target path is at the right spot and is actually a + # bind-mount of the correct inode. + update_config '.process.args = ["stat", "-c", "%n %d:%i", "--", "'"$real_dst"'"]' + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == "$real_dst $(stat -c "%d:%i" -- "$src")" ]] + + # Make sure there is a mount entry for the target path. + # shellcheck disable=SC2016 + update_config '.process.args = ["awk", "-F", "PATH='"$real_dst"'", "$2 == PATH", "/proc/self/mounts"]' + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"$real_dst"* ]] + + # Switch back the old config so this function can be called multiple times. + mv "$old_config" "./config.json" +} + # https://github.com/opencontainers/runc/issues/3991 @test "runc run [tmpcopyup]" { mkdir -p rootfs/dir1/dir2 @@ -343,3 +379,25 @@ function test_mount_order() { @test "runc run [mount order, container idmap source] (userns)" { test_mount_order userns,idmap } + +@test "runc run [bind mount through a dangling symlink component]" { + rm -rf rootfs/etc/hosts + ln -s /tmp/foo/bar rootfs/jump + ln -s /jump/baz/hosts rootfs/etc/hosts + + rm -rf rootfs/tmp/foo + test_mount_target ./config.json /etc/hosts /tmp/foo/bar/baz/hosts +} + +@test "runc run [bind mount through a trailing dangling symlink]" { + rm -rf rootfs/etc/hosts + ln -s /tmp/hosts rootfs/etc/hosts + + # File. + rm -rf rootfs/tmp/hosts + test_mount_target ./config.json /etc/hosts /tmp/hosts + + # Directory. + rm -rf rootfs/tmp/hosts + test_mount_target . /etc/hosts /tmp/hosts +}