mirror of
https://github.com/opencontainers/runc.git
synced 2026-11-06 05:17:05 +00:00
setReadonly remounts / with MS_BIND|MS_REMOUNT|MS_RDONLY. A bind remount sets the per-mount flags to exactly the ones given, so when not in a user namespace it silently clears nosuid, nodev, noexec and nosymfollow which may be set on the rootfs mount. This does not happen in a user namespace, where these flags are locked, so the remount fails, and the fallback keeps them. The fix is to always use the existing flags, not only on fallback. In other words, the fallback is no longer needed. While at it - use statfsToMountFlags to convert, since ST_* and MS_* values may differ (e.g. ST_NOSYMFOLLOW != MS_NOSYMFOLLOW); - do not take other flags (e.g *ATIME*) as the kernel preserves those on remount. Add an integration test. Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
421 lines
12 KiB
Bash
421 lines
12 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
load helpers
|
|
|
|
function setup() {
|
|
setup_busybox
|
|
}
|
|
|
|
function teardown() {
|
|
umount -l "$ROOT/bundle/rootfs" 2>/dev/null || true
|
|
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 <https://github.com/koalaman/shellcheck/issues/2873>.
|
|
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"]'
|
|
run -0 runc run test_busybox
|
|
[ "${#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 <https://github.com/koalaman/shellcheck/issues/2873>.
|
|
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" "/mnt/x/y/z/x" "$ctr_src_opts"
|
|
config_add_bind_mount "$rootfs/mnt/x/y" "/mnt/x/y/z" "$ctr_src_opts"
|
|
# Finally, bind-mount the whole thing on top of /final.
|
|
config_add_bind_mount "$rootfs/mnt" "/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"]'
|
|
run -0 runc run test_busybox
|
|
assert_output --partial "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 <https://github.com/koalaman/shellcheck/issues/2873>.
|
|
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"'"]'
|
|
run -0 runc run test_busybox
|
|
assert_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"]'
|
|
run -0 runc run test_busybox
|
|
assert_output --partial "$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
|
|
run -0 runc run test_busybox
|
|
assert_line --index 0 --partial 'drwxrwxrwx'
|
|
}
|
|
|
|
@test "runc run [bind mount]" {
|
|
update_config ' .mounts += [{
|
|
source: ".",
|
|
destination: "/tmp/bind",
|
|
options: ["bind"]
|
|
}]
|
|
| .process.args |= ["ls", "/tmp/bind/config.json"]'
|
|
|
|
run -0 runc run test_busybox
|
|
assert_line --index 0 --partial '/tmp/bind/config.json'
|
|
}
|
|
|
|
@test "runc run [bind mount with rootfs-prefixed destination]" {
|
|
# Mount destination is a path inside the container,
|
|
# even if it starts with the host path to rootfs.
|
|
dest="$(pwd -P)/rootfs/tmp/bind"
|
|
update_config ' .mounts += [{
|
|
source: ".",
|
|
destination: "'"$dest"'",
|
|
options: ["bind"]
|
|
}]
|
|
| .process.args |= ["ls", "'"$dest"'/config.json"]'
|
|
|
|
run -0 runc run test_busybox
|
|
assert_line --index 0 --partial "$dest/config.json"
|
|
# The prefix is not stripped.
|
|
[ ! -e rootfs/tmp/bind ]
|
|
}
|
|
|
|
# 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"]'
|
|
|
|
run -0 runc run test_busybox
|
|
assert_line --index 0 --partial '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"]'
|
|
|
|
run -0 runc run test_busybox
|
|
assert_line --index 0 --partial '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"]'
|
|
run -0 runc run test_busybox
|
|
}
|
|
|
|
# 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.
|
|
run ! runc run test_busybox
|
|
assert_output --regexp 'proc.*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"]'
|
|
|
|
run -0 runc run test_busybox
|
|
|
|
# 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"]'
|
|
|
|
run -0 runc run test_busybox
|
|
assert_output "710"
|
|
|
|
update_config '.process.args = ["cat", "/proc/self/mounts"]'
|
|
run -0 runc run test_busybox
|
|
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.
|
|
run -0 runc run test_busybox
|
|
assert_output "1500"
|
|
|
|
update_config '.process.args = ["cat", "/proc/self/mounts"]'
|
|
run -0 runc run test_busybox
|
|
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
|
|
run -0 runc run test_busybox
|
|
assert_output "1777"
|
|
|
|
update_config '.process.args = ["cat", "/proc/self/mounts"]'
|
|
|
|
rm -rf rootfs/non-existent
|
|
run -0 runc run test_busybox
|
|
# 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
|
|
}
|
|
|
|
@test "runc run [readonly rootfs keeps nosuid,nodev]" {
|
|
requires root
|
|
|
|
mount --bind rootfs rootfs
|
|
mount -o remount,bind,nosuid,nodev rootfs
|
|
|
|
update_config '.root.readonly = true'
|
|
# shellcheck disable=SC2016
|
|
update_config --arg prog '$5 == "/" {print "," $6 ","}' \
|
|
'.process.args = ["awk", $prog, "/proc/self/mountinfo"]'
|
|
run -0 runc run test_busybox
|
|
assert_output --partial ",ro,"
|
|
assert_output --partial ",nosuid,"
|
|
assert_output --partial ",nodev,"
|
|
}
|