Skip to content

Commit 0d72adf

Browse files
committed
Prohibit /proc and /sys to be symlinks
Commit 3291d66 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 0ca91f4 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 <[email protected]>
1 parent 206008a commit 0d72adf

File tree

2 files changed

+39
-9
lines changed

2 files changed

+39
-9
lines changed

libcontainer/rootfs_linux.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -375,32 +375,43 @@ func doTmpfsCopyUp(m *configs.Mount, rootfs, mountLabel string) (Err error) {
375375

376376
func mountToRootfs(m *configs.Mount, c *mountConfig) error {
377377
rootfs := c.root
378-
mountLabel := c.label
379-
mountFd := c.fd
380-
dest, err := securejoin.SecureJoin(rootfs, m.Destination)
381-
if err != nil {
382-
return err
383-
}
384378

379+
// procfs and sysfs are special because we need to ensure they are actually
380+
// mounted on a specific path in a container without any funny business.
385381
switch m.Device {
386382
case "proc", "sysfs":
387383
// If the destination already exists and is not a directory, we bail
388-
// out This is to avoid mounting through a symlink or similar -- which
384+
// out. This is to avoid mounting through a symlink or similar -- which
389385
// has been a "fun" attack scenario in the past.
390386
// TODO: This won't be necessary once we switch to libpathrs and we can
391387
// stop all of these symlink-exchange attacks.
388+
dest := filepath.Clean(m.Destination)
389+
if !strings.HasPrefix(dest, rootfs) {
390+
// Do not use securejoin as it resolves symlinks.
391+
dest = filepath.Join(rootfs, dest)
392+
}
392393
if fi, err := os.Lstat(dest); err != nil {
393394
if !os.IsNotExist(err) {
394395
return err
395396
}
396-
} else if fi.Mode()&os.ModeDir == 0 {
397+
} else if !fi.IsDir() {
397398
return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device)
398399
}
399400
if err := os.MkdirAll(dest, 0o755); err != nil {
400401
return err
401402
}
402-
// Selinux kernels do not support labeling of /proc or /sys
403+
// Selinux kernels do not support labeling of /proc or /sys.
403404
return mountPropagate(m, rootfs, "", nil)
405+
}
406+
407+
mountLabel := c.label
408+
mountFd := c.fd
409+
dest, err := securejoin.SecureJoin(rootfs, m.Destination)
410+
if err != nil {
411+
return err
412+
}
413+
414+
switch m.Device {
404415
case "mqueue":
405416
if err := os.MkdirAll(dest, 0o755); err != nil {
406417
return err

tests/integration/mask.bats

+19
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,22 @@ function teardown() {
5656
[ "$status" -eq 1 ]
5757
[[ "${output}" == *"Operation not permitted"* ]]
5858
}
59+
60+
@test "mask paths [prohibit symlink /proc]" {
61+
ln -s /symlink rootfs/proc
62+
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
63+
[ "$status" -eq 1 ]
64+
[[ "${output}" == *"must be mounted on ordinary directory"* ]]
65+
}
66+
67+
@test "mask paths [prohibit symlink /sys]" {
68+
# In rootless containers, /sys is a bind mount not a real sysfs.
69+
requires root
70+
71+
ln -s /symlink rootfs/sys
72+
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
73+
[ "$status" -eq 1 ]
74+
# On cgroup v1, this may fail before checking if /sys is a symlink,
75+
# so we merely check that it fails, and do not check the exact error
76+
# message like for /proc above.
77+
}

0 commit comments

Comments
 (0)