Merge pull request #5275 from lifubang/maskdir-with-shared-tmpfs

[Carry #5262] libct: reuse tmpfs for directory masks
This commit is contained in:
Sebastiaan van Stijn
2026-05-13 09:37:43 +02:00
committed by GitHub
3 changed files with 101 additions and 6 deletions
+46 -3
View File
@@ -419,7 +419,7 @@ func mountCgroupV2(m mountEntry, c *mountConfig) error {
// Mask `/sys/fs/cgroup` to ensure it is read-only, even when `/sys` is mounted // Mask `/sys/fs/cgroup` to ensure it is read-only, even when `/sys` is mounted
// with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`). // with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`).
err = utils.WithProcfdFile(m.dstFile, func(procfd string) error { err = utils.WithProcfdFile(m.dstFile, func(procfd string) error {
return maskPaths([]string{procfd}, c.label) return maskPaths(c.root, []string{procfd}, c.label)
}) })
} }
return err return err
@@ -1333,7 +1333,7 @@ func verifyDevNull(f *os.File) error {
// mounts ( proc/kcore ). // mounts ( proc/kcore ).
// For files, maskPath bind mounts /dev/null over the top of the specified path. // For files, maskPath bind mounts /dev/null over the top of the specified path.
// For directories, maskPath mounts read-only tmpfs over the top of the specified path. // For directories, maskPath mounts read-only tmpfs over the top of the specified path.
func maskPaths(paths []string, mountLabel string) error { func maskPaths(rootFd *os.File, paths []string, mountLabel string) error {
devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0) devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0)
if err != nil { if err != nil {
return fmt.Errorf("can't mask paths: %w", err) return fmt.Errorf("can't mask paths: %w", err)
@@ -1346,6 +1346,17 @@ func maskPaths(paths []string, mountLabel string) error {
procSelfFd, closer := utils.ProcThreadSelf("fd/") procSelfFd, closer := utils.ProcThreadSelf("fd/")
defer closer() defer closer()
var (
sharedMaskFile *os.File
sharedMaskSrc *mountSource
bindFailed bool
)
defer func() {
if sharedMaskFile != nil {
_ = sharedMaskFile.Close()
}
}()
maskedPaths := make(map[string]struct{})
for _, path := range paths { for _, path := range paths {
// Open the target path; skip if it doesn't exist. // Open the target path; skip if it doesn't exist.
dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0) dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
@@ -1360,11 +1371,43 @@ func maskPaths(paths []string, mountLabel string) error {
dstFh.Close() dstFh.Close()
return fmt.Errorf("can't mask path %q: %w", path, err) return fmt.Errorf("can't mask path %q: %w", path, err)
} }
// skip duplicate masked paths.
cleanPath := pathrs.LexicallyCleanPath(path)
if _, ok := maskedPaths[cleanPath]; ok {
dstFh.Close()
continue
}
maskedPaths[cleanPath] = struct{}{}
var dstType string var dstType string
if st.IsDir() { if st.IsDir() {
// Destination is a directory: bind mount a ro tmpfs over it. // Destination is a directory: bind mount a ro tmpfs over it.
dstType = "dir" dstType = "dir"
err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel)) if !bindFailed && sharedMaskSrc != nil {
dstFd := filepath.Join(procSelfFd, strconv.Itoa(int(dstFh.Fd())))
err = mountViaFds("", sharedMaskSrc, path, dstFd, "", unix.MS_BIND, "")
if err != nil {
// A bind-mount inherits MNT_READONLY from the source vfsmount,
// but if it fails fall back to individual tmpfs mounts.
bindFailed = true
logrus.WithError(err).Warn("maskPaths: shared tmpfs bind-mount failed, falling back to per-directory tmpfs")
}
}
if bindFailed || sharedMaskSrc == nil {
err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("nr_blocks=1,nr_inodes=1", mountLabel))
if err == nil && !bindFailed && sharedMaskSrc == nil {
// Establish this mount as the reusable shared source. reopenAfterMount
// resolves the underlying inode via procfs and re-opens it through
// rootFd, so the resulting fd is anchored to the real path inside the
// container rootfs even if path was a /proc/self/fd/N alias.
reopened, err := reopenAfterMount(rootFd, dstFh, unix.O_PATH|unix.O_CLOEXEC)
if err != nil {
return fmt.Errorf("can't reopen shared directory mask: %w", err)
}
sharedMaskFile = reopened
sharedMaskSrc = &mountSource{Type: mountSourcePlain, file: sharedMaskFile}
}
}
} else { } else {
// Destination is a file: mount it to /dev/null. // Destination is a file: mount it to /dev/null.
dstType = "path" dstType = "path"
+10 -2
View File
@@ -142,8 +142,16 @@ func (l *linuxStandardInit) Init() error {
} }
} }
if err := maskPaths(l.config.Config.MaskPaths, l.config.Config.MountLabel); err != nil { if len(l.config.Config.MaskPaths) > 0 {
return err rootFd, err := os.OpenFile("/", unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_PATH, 0)
if err != nil {
return fmt.Errorf("open rootfs handle for masked paths: %w", err)
}
err = maskPaths(rootFd, l.config.Config.MaskPaths, l.config.Config.MountLabel)
rootFd.Close()
if err != nil {
return err
}
} }
pdeath, err := system.GetParentDeathSignal() pdeath, err := system.GetParentDeathSignal()
if err != nil { if err != nil {
+45 -1
View File
@@ -6,7 +6,7 @@ function setup() {
setup_busybox setup_busybox
# Create fake rootfs. # Create fake rootfs.
mkdir rootfs/testdir mkdir rootfs/testdir rootfs/testdir2 rootfs/testdir3
echo "Forbidden information!" >rootfs/testfile echo "Forbidden information!" >rootfs/testfile
# add extra masked paths # add extra masked paths
@@ -55,6 +55,20 @@ function teardown() {
[[ "${output}" == *"Operation not permitted"* ]] [[ "${output}" == *"Operation not permitted"* ]]
} }
@test "mask paths [duplicate paths]" {
update_config '(.. | select(.maskedPaths? != null)) .maskedPaths += ["/testdir", "/testfile"]'
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
[ "$status" -eq 0 ]
runc exec test_busybox sh -c "mount | grep /testdir -c"
[ "$status" -eq 0 ]
[[ "${output}" == "1" ]]
runc exec test_busybox sh -c "mount | grep /testfile -c"
[ "$status" -eq 0 ]
[[ "${output}" == "1" ]]
}
@test "mask paths [prohibit symlink /proc]" { @test "mask paths [prohibit symlink /proc]" {
ln -s /symlink rootfs/proc ln -s /symlink rootfs/proc
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
@@ -73,3 +87,33 @@ function teardown() {
# so we merely check that it fails, and do not check the exact error # so we merely check that it fails, and do not check the exact error
# message like for /proc above. # message like for /proc above.
} }
@test "mask paths [directories share tmpfs]" {
update_config '(.. | select(.maskedPaths? != null)) .maskedPaths += ["/testdir2", "/testdir3"]'
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
[ "$status" -eq 0 ]
# shellcheck disable=SC2016
runc exec test_busybox sh -euc '
set -- $(stat -c %d /testdir /testdir2 /testdir3)
[ "$1" = "$2" ]
[ "$2" = "$3" ]
'
[ "$status" -eq 0 ]
runc exec test_busybox touch /testdir2/foo
[ "$status" -eq 1 ]
[[ "${output}" == *"Read-only file system"* ]]
}
@test "mask paths [directory with read-only rootfs]" {
update_config '(.. | select(.maskedPaths? != null)) .maskedPaths += ["/testdir2", "/testdir3"]'
update_config '.root.readonly = true'
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
[ "$status" -eq 0 ]
runc exec test_busybox ls /testdir
[ "$status" -eq 0 ]
[ -z "$output" ]
}