From 31a22b7c49055305812eb712e4dec9e6e7fd4679 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 16 Mar 2023 14:35:50 -0700 Subject: [PATCH] Prohibit /proc and /sys to be symlinks Commit 3291d66b9844 introduced a check for /proc and /sys, making sure the destination (dest) is a directory (and not e.g. a symlink). Later, a hunk from commit 0ca91f44f switched from using filepath.Join to SecureJoin for dest. As SecureJoin follows and resolves symlinks, the check whether dest is a symlink no longer works. To fix, do the check without/before using SecureJoin. Add integration tests to make sure we won't regress. Signed-off-by: Kir Kolyshkin --- libcontainer/rootfs_linux.go | 22 ++++++++++++++++------ tests/integration/mask.bats | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 2a98372b561..e57cad347bd 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -377,25 +377,35 @@ func mountToRootfs(m *configs.Mount, c *mountConfig) error { rootfs := c.root mountLabel := c.label mountFd := c.fd - dest, err := securejoin.SecureJoin(rootfs, m.Destination) - if err != nil { - return err - } switch m.Device { case "proc", "sysfs": // If the destination already exists and is not a directory, we bail - // out This is to avoid mounting through a symlink or similar -- which + // out. This is to avoid mounting through a symlink or similar -- which // has been a "fun" attack scenario in the past. // TODO: This won't be necessary once we switch to libpathrs and we can // stop all of these symlink-exchange attacks. + dest := m.Destination + if !strings.HasPrefix(dest, rootfs) { + // Do not use securejoin as it resolves symlinks. + dest = filepath.Join(rootfs, dest) + } if fi, err := os.Lstat(dest); err != nil { if !os.IsNotExist(err) { return err } - } else if fi.Mode()&os.ModeDir == 0 { + } else if !fi.IsDir() { return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device) } + } + + dest, err := securejoin.SecureJoin(rootfs, m.Destination) + if err != nil { + return err + } + + switch m.Device { + case "proc", "sysfs": if err := os.MkdirAll(dest, 0o755); err != nil { return err } diff --git a/tests/integration/mask.bats b/tests/integration/mask.bats index 4bf834ef92f..4c0af48e09c 100644 --- a/tests/integration/mask.bats +++ b/tests/integration/mask.bats @@ -56,3 +56,19 @@ function teardown() { [ "$status" -eq 1 ] [[ "${output}" == *"Operation not permitted"* ]] } + +@test "mask paths [prohibit symlink /proc]" { + ln -s /symlink rootfs/proc + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox + [ "$status" -eq 1 ] + [[ "${output}" == *"must be mounted on ordinary directory"* ]] +} + +@test "mask paths [prohibit symlink /sys]" { + ln -s /symlink rootfs/sys + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox + [ "$status" -eq 1 ] + # On cgroup v1, this may fail before checking if /sys is a symlink, + # so we merely check that it fails, and do not check the exact error + # message like for /proc above. +}