#!/usr/bin/env bats load helpers function setup() { setup_busybox } function teardown() { teardown_bundle } # https://github.com/opencontainers/runc/security/advisories/GHSA-m8cg-xc2p-r3fc # # This needs to be placed at the top of the bats file to work around # a shellcheck bug. See . function test_ro_cgroup_mount() { local lines status # shellcheck disable=SC2016 update_config '.process.args |= ["sh", "-euc", "for f in `grep /sys/fs/cgroup /proc/mounts | awk \"{print \\\\$2}\"| uniq`; do test -e $f && grep -w $f /proc/mounts | tail -n1; done"]' runc run test_busybox [ "$status" -eq 0 ] [ "${#lines[@]}" -ne 0 ] for line in "${lines[@]}"; do [[ "${line}" == *'ro,'* ]]; done } # Parse an "optstring" of the form foo,bar into $is_foo and $is_bar variables. # Usage: parse_optstring foo,bar foo bar baz function parse_optstring() { optstring="$1" shift for flag in "$@"; do is_set= if grep -wq "$flag" <<<"$optstring"; then is_set=1 fi eval "is_$flag=$is_set" done } function config_add_bind_mount() { src="$1" dst="$2" parse_optstring "${3:-}" rbind idmap bindtype=bind if [ -n "$is_rbind" ]; then bindtype=rbind fi mappings="" if [ -n "$is_idmap" ]; then mappings=' "uidMappings": [{"containerID": 0, "hostID": 100000, "size": 65536}], "gidMappings": [{"containerID": 0, "hostID": 100000, "size": 65536}], ' fi update_config '.mounts += [{ "source": "'"$src"'", "destination": "'"$dst"'", "type": "bind", '"$mappings"' "options": [ "'"$bindtype"'", "rprivate" ] }]' } # This needs to be placed at the top of the bats file to work around # a shellcheck bug. See . function test_mount_order() { parse_optstring "${1:-}" userns idmap if [ -n "$is_userns" ]; then requires root update_config '.linux.namespaces += [{"type": "user"}] | .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}] | .linux.gidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]' remap_rootfs fi ctr_src_opts="rbind" if [ -n "$is_idmap" ]; then requires root requires_kernel 5.12 requires_idmap_fs . ctr_src_opts+=",idmap" fi mkdir -p rootfs/{mnt,final} # Create a set of directories we can create a mount tree with. for subdir in a/x b/y c/z; do dir="bind-src/$subdir" mkdir -p "$dir" echo "$subdir" >"$dir/file" # Add a symlink to make sure topdir="$(dirname "$subdir")" ln -s "$topdir" "bind-src/sym-$topdir" done # In userns tests, make sure that the source directory cannot be accessed, # to make sure we're exercising the bind-mount source fd logic. chmod o-rwx bind-src rootfs="$(pwd)/rootfs" rm -rf rootfs/mnt mkdir rootfs/mnt # Create a bind-mount tree. config_add_bind_mount "$PWD/bind-src/a" "/mnt" config_add_bind_mount "$PWD/bind-src/sym-b" "/mnt/x" config_add_bind_mount "$PWD/bind-src/c" "/mnt/x/y" config_add_bind_mount "$PWD/bind-src/sym-a" "/mnt/x/y/z" # Create a recursive bind-mount that uses part of the current tree in the # container. config_add_bind_mount "$rootfs/mnt/x" "$rootfs/mnt/x/y/z/x" "$ctr_src_opts" config_add_bind_mount "$rootfs/mnt/x/y" "$rootfs/mnt/x/y/z" "$ctr_src_opts" # Finally, bind-mount the whole thing on top of /final. config_add_bind_mount "$rootfs/mnt" "$rootfs/final" "$ctr_src_opts" # Check that the entire tree was copied and the mounts were done in the # expected order. update_config '.process.args = ["cat", "/final/x/y/z/z/x/y/z/x/file"]' runc run test_busybox [ "$status" -eq 0 ] [[ "$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 chmod 777 rootfs/dir1/dir2 update_config ' .mounts += [{ source: "tmpfs", destination: "/dir1", type: "tmpfs", options: ["tmpcopyup"] }] | .process.args |= ["ls", "-ld", "/dir1/dir2"]' umask 022 runc run test_busybox [ "$status" -eq 0 ] [[ "${lines[0]}" == *'drwxrwxrwx'* ]] } @test "runc run [bind mount]" { update_config ' .mounts += [{ source: ".", destination: "/tmp/bind", options: ["bind"] }] | .process.args |= ["ls", "/tmp/bind/config.json"]' runc run test_busybox [ "$status" -eq 0 ] [[ "${lines[0]}" == *'/tmp/bind/config.json'* ]] } # https://github.com/opencontainers/runc/issues/2246 @test "runc run [ro tmpfs mount]" { update_config ' .mounts += [{ source: "tmpfs", destination: "/mnt", type: "tmpfs", options: ["ro", "nodev", "nosuid", "mode=755"] }] | .process.args |= ["grep", "^tmpfs /mnt", "/proc/mounts"]' runc run test_busybox [ "$status" -eq 0 ] [[ "${lines[0]}" == *'ro,'* ]] } # https://github.com/opencontainers/runc/issues/3248 @test "runc run [ro /dev mount]" { update_config ' .mounts |= map((select(.destination == "/dev") | .options += ["ro"]) // .) | .process.args |= ["grep", "^tmpfs /dev", "/proc/mounts"]' runc run test_busybox [ "$status" -eq 0 ] [[ "${lines[0]}" == *'ro,'* ]] } # https://github.com/opencontainers/runc/issues/2683 @test "runc run [tmpfs mount with absolute symlink]" { # in container, /conf -> /real/conf mkdir -p rootfs/real/conf ln -s /real/conf rootfs/conf update_config ' .mounts += [{ type: "tmpfs", source: "tmpfs", destination: "/conf/stack", options: ["ro", "nodev", "nosuid"] }] | .process.args |= ["true"]' runc run test_busybox [ "$status" -eq 0 ] } # CVE-2023-27561 CVE-2019-19921 @test "runc run [/proc is a symlink]" { # Make /proc in the container a symlink. rm -rf rootfs/proc mkdir -p rootfs/bad-proc ln -sf /bad-proc rootfs/proc # This should fail. runc run test_busybox [ "$status" -ne 0 ] [[ "$output" == *"must be mounted on ordinary directory"* ]] } # https://github.com/opencontainers/runc/issues/4401 @test "runc run [setgid / + mkdirall]" { mkdir rootfs/setgid chmod '=7755' rootfs/setgid update_config '.mounts += [{ type: "tmpfs", source: "tmpfs", destination: "/setgid/a/b/c", options: ["ro", "nodev", "nosuid"] }]' update_config '.process.args |= ["true"]' runc run test_busybox [ "$status" -eq 0 ] # Verify that the setgid bit is inherited. [[ "$(stat -c %a rootfs/setgid)" == 7755 ]] [[ "$(stat -c %a rootfs/setgid/a)" == 2755 ]] [[ "$(stat -c %a rootfs/setgid/a/b)" == 2755 ]] [[ "$(stat -c %a rootfs/setgid/a/b/c)" == 2755 ]] } # https://github.com/opencontainers/runc/issues/4971 @test "runc run [tmpfs mount mode= inherit]" { mkdir rootfs/tmpfs chmod "=0710" rootfs/tmpfs update_config '.mounts += [{ type: "tmpfs", source: "tmpfs", destination: "/tmpfs", options: ["rw", "nodev", "nosuid"] }]' update_config '.process.args = ["stat", "-c", "%a", "/tmpfs"]' runc run test_busybox [ "$status" -eq 0 ] [[ "$output" == "710" ]] update_config '.process.args = ["cat", "/proc/self/mounts"]' runc run test_busybox [ "$status" -eq 0 ] grep -Ex "tmpfs /tmpfs tmpfs [^ ]*\bmode=710\b[^ ]* .*" <<<"$output" } # https://github.com/opencontainers/runc/issues/4971 @test "runc run [tmpfs mount explicit mode=]" { mkdir rootfs/tmpfs chmod "=0710" rootfs/tmpfs update_config '.mounts += [{ type: "tmpfs", source: "tmpfs", destination: "/tmpfs", options: ["rw", "nodev", "nosuid", "mode=1500"] }]' update_config '.process.args = ["stat", "-c", "%a", "/tmpfs"]' # Explicitly setting mode= overrides whatever mode we would've inherited. runc run test_busybox [ "$status" -eq 0 ] [[ "$output" == "1500" ]] update_config '.process.args = ["cat", "/proc/self/mounts"]' runc run test_busybox [ "$status" -eq 0 ] grep -Ex "tmpfs /tmpfs tmpfs [^ ]*\bmode=1500\b[^ ]* .*" <<<"$output" # Verify that the actual directory was not chmod-ed. [[ "$(stat -c %a rootfs/tmpfs)" == 710 ]] } # https://github.com/opencontainers/runc/issues/4971 @test "runc run [tmpfs mount mode=1777 default]" { update_config '.mounts += [{ type: "tmpfs", source: "tmpfs", destination: "/non-existent/foo/bar/baz", options: ["rw", "nodev", "nosuid"] }]' update_config '.process.args = ["stat", "-c", "%a", "/non-existent/foo/bar/baz"]' rm -rf rootfs/non-existent runc run test_busybox [ "$status" -eq 0 ] [[ "$output" == "1777" ]] update_config '.process.args = ["cat", "/proc/self/mounts"]' rm -rf rootfs/non-existent runc run test_busybox [ "$status" -eq 0 ] # We don't explicitly set a mode= in this case, it is just the tmpfs default. grep -Ex "tmpfs /non-existent/foo/bar/baz tmpfs .*" <<<"$output" run ! grep -Ex "tmpfs /non-existent/foo/bar/baz tmpfs [^ ]*\bmode=[0-7]+\b[^ ]* .*" <<<"$output" # Verify that the actual modes are *not* 1777. [[ "$(stat -c %a rootfs/non-existent)" == 755 ]] [[ "$(stat -c %a rootfs/non-existent/foo)" == 755 ]] [[ "$(stat -c %a rootfs/non-existent/foo/bar)" == 755 ]] [[ "$(stat -c %a rootfs/non-existent/foo/bar/baz)" == 755 ]] } @test "runc run [ro /sys/fs/cgroup mounts]" { # Without cgroup namespace. update_config '.linux.namespaces -= [{"type": "cgroup"}]' test_ro_cgroup_mount } @test "runc run [ro /sys/fs/cgroup mounts + cgroupns]" { requires cgroupns # With cgroup namespace. update_config '.linux.namespaces |= if index({"type": "cgroup"}) then . else . + [{"type": "cgroup"}] end' test_ro_cgroup_mount } @test "runc run [mount order, container bind-mount source]" { test_mount_order } @test "runc run [mount order, container bind-mount source] (userns)" { test_mount_order userns } @test "runc run [mount order, container idmap source]" { test_mount_order idmap } @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 }