diff options
Diffstat (limited to 'test/units/TEST-13-NSPAWN.nspawn.sh')
-rwxr-xr-x | test/units/TEST-13-NSPAWN.nspawn.sh | 977 |
1 files changed, 977 insertions, 0 deletions
diff --git a/test/units/TEST-13-NSPAWN.nspawn.sh b/test/units/TEST-13-NSPAWN.nspawn.sh new file mode 100755 index 0000000..7901e98 --- /dev/null +++ b/test/units/TEST-13-NSPAWN.nspawn.sh @@ -0,0 +1,977 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +# +# Notes on coverage: when collecting coverage we need the $BUILD_DIR present +# and writable in the container as well. To do this in the least intrusive way, +# two things are going on in the background (only when built with -Db_coverage=true): +# 1) the systemd-nspawn@.service is copied to /etc/systemd/system/ with +# --bind=$BUILD_DIR appended to the ExecStart= line +# 2) each create_dummy_container() call also creates an .nspawn file in /run/systemd/nspawn/ +# with the last fragment from the path used as a name +# +# The first change is quite self-contained and applies only to containers run +# with machinectl. The second one might cause some unexpected side-effects, namely: +# - nspawn config (setting) files don't support dropins, so tests that test +# the config files might need some tweaking (as seen below with +# the $COVERAGE_BUILD_DIR shenanigans) since they overwrite the .nspawn file +# - also a note - if /etc/systemd/nspawn/cont-name.nspawn exists, it takes +# precedence and /run/systemd/nspawn/cont-name.nspawn won't be read even +# if it exists +# - also a note 2 - --bind= overrides any Bind= from a config file +# - in some cases we don't create a test container using create_dummy_container(), +# so in that case an explicit call to coverage_create_nspawn_dropin() is needed +# +# However, even after jumping through all these hooks, there still might (and is) +# some "incorrectly" missing coverage, especially in the window between spawning +# the inner child process and bind-mounting the coverage $BUILD_DIR +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + + +export SYSTEMD_LOG_LEVEL=debug +export SYSTEMD_LOG_TARGET=journal + +at_exit() { + set +e + + mountpoint -q /var/lib/machines && umount --recursive /var/lib/machines + rm -f /run/systemd/nspawn/*.nspawn +} + +trap at_exit EXIT + +# check cgroup-v2 +IS_CGROUPSV2_SUPPORTED=no +mkdir -p /tmp/cgroup2 +if mount -t cgroup2 cgroup2 /tmp/cgroup2; then + IS_CGROUPSV2_SUPPORTED=yes + umount /tmp/cgroup2 +fi +rmdir /tmp/cgroup2 + +# check cgroup namespaces +IS_CGNS_SUPPORTED=no +if [[ -f /proc/1/ns/cgroup ]]; then + IS_CGNS_SUPPORTED=yes +fi + +IS_USERNS_SUPPORTED=no +# On some systems (e.g. CentOS 7) the default limit for user namespaces +# is set to 0, which causes the following unshare syscall to fail, even +# with enabled user namespaces support. By setting this value explicitly +# we can ensure the user namespaces support to be detected correctly. +sysctl -w user.max_user_namespaces=10000 +if unshare -U bash -c :; then + IS_USERNS_SUPPORTED=yes +fi + +# Mount temporary directory over /var/lib/machines to not pollute the image +mkdir -p /var/lib/machines +mount --bind "$(mktemp --tmpdir=/var/tmp -d)" /var/lib/machines + +testcase_sanity() { + local template root image uuid tmpdir + + tmpdir="$(mktemp -d)" + template="$(mktemp -d /tmp/nspawn-template.XXX)" + create_dummy_container "$template" + # Create a simple image from the just created container template + image="$(mktemp /var/lib/machines/TEST-13-NSPAWN.image-XXX.img)" + dd if=/dev/zero of="$image" bs=1M count=256 + mkfs.ext4 "$image" + mkdir -p /mnt + mount -o loop "$image" /mnt + cp -r "$template"/* /mnt/ + umount /mnt + + systemd-nspawn --help --no-pager + systemd-nspawn --version + + # --template= + root="$(mktemp -u -d /var/lib/machines/TEST-13-NSPAWN.sanity.XXX)" + coverage_create_nspawn_dropin "$root" + (! systemd-nspawn --directory="$root" bash -xec 'echo hello') + # Initialize $root from $template (the $root directory must not exist, hence + # the `mktemp -u` above) + systemd-nspawn --directory="$root" --template="$template" bash -xec 'echo hello' + systemd-nspawn --directory="$root" bash -xec 'echo hello; touch /initialized' + test -e "$root/initialized" + # Check if the $root doesn't get re-initialized once it's not empty + systemd-nspawn --directory="$root" --template="$template" bash -xec 'echo hello' + test -e "$root/initialized" + + systemd-nspawn --directory="$root" --ephemeral bash -xec 'touch /ephemeral' + test ! -e "$root/ephemeral" + (! systemd-nspawn --directory="$root" \ + --read-only \ + bash -xec 'touch /nope') + test ! -e "$root/nope" + systemd-nspawn --image="$image" bash -xec 'echo hello' + + # --volatile= + touch "$root/usr/has-usr" + # volatile(=yes): rootfs is tmpfs, /usr/ from the OS tree is mounted read only + systemd-nspawn --directory="$root"\ + --volatile \ + bash -xec 'test -e /usr/has-usr; touch /usr/read-only && exit 1; touch /nope' + test ! -e "$root/nope" + test ! -e "$root/usr/read-only" + systemd-nspawn --directory="$root"\ + --volatile=yes \ + bash -xec 'test -e /usr/has-usr; touch /usr/read-only && exit 1; touch /nope' + test ! -e "$root/nope" + test ! -e "$root/usr/read-only" + # volatile=state: rootfs is read-only, /var/ is tmpfs + systemd-nspawn --directory="$root" \ + --volatile=state \ + bash -xec 'test -e /usr/has-usr; mountpoint /var; touch /read-only && exit 1; touch /var/nope' + test ! -e "$root/read-only" + test ! -e "$root/var/nope" + # volatile=state: tmpfs overlay is mounted over rootfs + systemd-nspawn --directory="$root" \ + --volatile=overlay \ + bash -xec 'test -e /usr/has-usr; touch /nope; touch /var/also-nope; touch /usr/nope-too' + test ! -e "$root/nope" + test ! -e "$root/var/also-nope" + test ! -e "$root/usr/nope-too" + + # --machine=, --hostname= + systemd-nspawn --directory="$root" \ + --machine="foo-bar.baz" \ + bash -xec '[[ $(hostname) == foo-bar.baz ]]' + systemd-nspawn --directory="$root" \ + --hostname="hello.world.tld" \ + bash -xec '[[ $(hostname) == hello.world.tld ]]' + systemd-nspawn --directory="$root" \ + --machine="foo-bar.baz" \ + --hostname="hello.world.tld" \ + bash -xec '[[ $(hostname) == hello.world.tld ]]' + + # --uuid= + rm -f "$root/etc/machine-id" + uuid="deadbeef-dead-dead-beef-000000000000" + systemd-nspawn --directory="$root" \ + --uuid="$uuid" \ + bash -xec "[[ \$container_uuid == $uuid ]]" + + # --as-pid2 + systemd-nspawn --directory="$root" bash -xec '[[ $$ -eq 1 ]]' + systemd-nspawn --directory="$root" --as-pid2 bash -xec '[[ $$ -eq 2 ]]' + + # --user= + # "Fake" getent passwd's bare minimum, so we don't have to pull it in + # with all the DSO shenanigans + cat >"$root/bin/getent" <<\EOF +#!/bin/bash + +if [[ $# -eq 0 ]]; then + : +elif [[ $1 == passwd ]]; then + echo "testuser:x:1000:1000:testuser:/:/bin/sh" +elif [[ $1 == initgroups ]]; then + echo "testuser" +fi +EOF + chmod +x "$root/bin/getent" + systemd-nspawn --directory="$root" bash -xec '[[ $USER == root ]]' + systemd-nspawn --directory="$root" --user=testuser bash -xec '[[ $USER == testuser ]]' + + # --settings= + .nspawn files + mkdir -p /run/systemd/nspawn/ + uuid="deadbeef-dead-dead-beef-000000000000" + echo -ne "[Exec]\nMachineID=deadbeef-dead-dead-beef-111111111111" >/run/systemd/nspawn/foo-bar.nspawn + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --settings=yes \ + bash -xec '[[ $container_uuid == deadbeef-dead-dead-beef-111111111111 ]]' + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --uuid="$uuid" \ + --settings=yes \ + bash -xec "[[ \$container_uuid == $uuid ]]" + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --uuid="$uuid" \ + --settings=override \ + bash -xec '[[ $container_uuid == deadbeef-dead-dead-beef-111111111111 ]]' + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --uuid="$uuid" \ + --settings=trusted \ + bash -xec "[[ \$container_uuid == $uuid ]]" + + # Mounts + mkdir "$tmpdir"/{1,2,3} + touch "$tmpdir/1/one" "$tmpdir/2/two" "$tmpdir/3/three" + touch "$tmpdir/foo" + # --bind= + systemd-nspawn --directory="$root" \ + ${COVERAGE_BUILD_DIR:+--bind="$COVERAGE_BUILD_DIR"} \ + --bind="$tmpdir:/foo" \ + --bind="$tmpdir:/also-foo:noidmap,norbind" \ + bash -xec 'test -e /foo/foo; touch /foo/bar; test -e /also-foo/bar' + test -e "$tmpdir/bar" + # --bind-ro= + systemd-nspawn --directory="$root" \ + --bind-ro="$tmpdir:/foo" \ + --bind-ro="$tmpdir:/bar:noidmap,norbind" \ + bash -xec 'test -e /foo/foo; touch /foo/baz && exit 1; touch /bar && exit 1; true' + # --inaccessible= + systemd-nspawn --directory="$root" \ + --inaccessible=/var \ + bash -xec 'touch /var/foo && exit 1; true' + # --tmpfs= + systemd-nspawn --directory="$root" \ + --tmpfs=/var:rw,nosuid,noexec \ + bash -xec 'touch /var/nope' + test ! -e "$root/var/nope" + # --overlay= + systemd-nspawn --directory="$root" \ + --overlay="$tmpdir/1:$tmpdir/2:$tmpdir/3:/var" \ + bash -xec 'test -e /var/one; test -e /var/two; test -e /var/three; touch /var/foo' + test -e "$tmpdir/3/foo" + # --overlay-ro= + systemd-nspawn --directory="$root" \ + --overlay-ro="$tmpdir/1:$tmpdir/2:$tmpdir/3:/var" \ + bash -xec 'test -e /var/one; test -e /var/two; test -e /var/three; touch /var/nope && exit 1; true' + test ! -e "$tmpdir/3/nope" + rm -fr "$tmpdir" + + # --port (sanity only) + systemd-nspawn --network-veth --directory="$root" --port=80 --port=90 true + systemd-nspawn --network-veth --directory="$root" --port=80:8080 true + systemd-nspawn --network-veth --directory="$root" --port=tcp:80 true + systemd-nspawn --network-veth --directory="$root" --port=tcp:80:8080 true + systemd-nspawn --network-veth --directory="$root" --port=udp:80 true + systemd-nspawn --network-veth --directory="$root" --port=udp:80:8080 --port=tcp:80:8080 true + (! systemd-nspawn --network-veth --directory="$root" --port= true) + (! systemd-nspawn --network-veth --directory="$root" --port=-1 true) + (! systemd-nspawn --network-veth --directory="$root" --port=: true) + (! systemd-nspawn --network-veth --directory="$root" --port=icmp:80:8080 true) + (! systemd-nspawn --network-veth --directory="$root" --port=tcp::8080 true) + (! systemd-nspawn --network-veth --directory="$root" --port=8080: true) + # Exercise adding/removing ports from an interface + systemd-nspawn --directory="$root" \ + --network-veth \ + --port=6667 \ + --port=80:8080 \ + --port=udp:53 \ + --port=tcp:22:2222 \ + bash -xec 'ip addr add dev host0 10.0.0.10/24; ip a; ip addr del dev host0 10.0.0.10/24' + + # --load-credential=, --set-credential= + echo "foo bar" >/tmp/cred.path + systemd-nspawn --directory="$root" \ + --load-credential=cred.path:/tmp/cred.path \ + --set-credential="cred.set:hello world" \ + bash -xec '[[ "$(</run/host/credentials/cred.path)" == "foo bar" ]]; [[ "$(</run/host/credentials/cred.set)" == "hello world" ]]' + # Combine with --user to ensure creds are still readable + systemd-nspawn --directory="$root" \ + --user=testuser \ + --no-new-privileges=yes \ + --load-credential=cred.path:/tmp/cred.path \ + --set-credential="cred.set:hello world" \ + bash -xec '[[ "$(</run/host/credentials/cred.path)" == "foo bar" ]]; [[ "$(</run/host/credentials/cred.set)" == "hello world" ]]' + rm -f /tmp/cred.path + + # Assorted tests + systemd-nspawn --directory="$root" --suppress-sync=yes bash -xec 'echo hello' + systemd-nspawn --capability=help + systemd-nspawn --resolv-conf=help + systemd-nspawn --timezone=help + + # Handling of invalid arguments + opts=( + bind + bind-ro + bind-user + chdir + console + inaccessible + kill-signal + link-journal + load-credential + network-{interface,macvlan,ipvlan,veth-extra,bridge,zone} + no-new-privileges + oom-score-adjust + overlay + overlay-ro + personality + pivot-root + port + private-users + private-users-ownership + register + resolv-conf + rlimit + root-hash + root-hash-sig + set-credential + settings + suppress-sync + timezone + tmpfs + uuid + ) + for opt in "${opts[@]}"; do + (! systemd-nspawn "--$opt") + [[ "$opt" == network-zone ]] && continue + (! systemd-nspawn "--$opt=''") + (! systemd-nspawn "--$opt=%\$ลก") + done + (! systemd-nspawn --volatile="") + (! systemd-nspawn --volatile=-1) + (! systemd-nspawn --rlimit==) +} + +nspawn_settings_cleanup() { + for dev in sd-host-only sd-shared{1,2,3} sd-macvlan{1,2} sd-ipvlan{1,2}; do + ip link del "$dev" || : + done + + return 0 +} + +testcase_nspawn_settings() { + local root container dev private_users wlan_names + + mkdir -p /run/systemd/nspawn + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.nspawn-settings.XXX)" + container="$(basename "$root")" + create_dummy_container "$root" + rm -f "/etc/systemd/nspawn/$container.nspawn" + mkdir -p "$root/tmp" "$root"/opt/{tmp,inaccessible,also-inaccessible} + + # add virtual wlan interfaces + if modprobe mac80211_hwsim radios=2; then + wlan_names="wlan0 wlan1:wl-renamed1" + fi + + for dev in sd-host-only sd-shared{1,2,3} sd-macvlan{1,2} sd-macvlanloong sd-ipvlan{1,2} sd-ipvlanlooong; do + ip link add "$dev" type dummy + done + udevadm settle + ip link property add dev sd-shared3 altname sd-altname3 altname sd-altname-tooooooooooooo-long + ip link + trap nspawn_settings_cleanup RETURN + + # Let's start with one huge config to test as much as we can at once + cat >"/run/systemd/nspawn/$container.nspawn" <<EOF +[Exec] +Boot=no +Ephemeral=no +ProcessTwo=no +Parameters=bash /entrypoint.sh "foo bar" 'bar baz' +Environment=FOO=bar +Environment=BAZ="hello world" +User=root +WorkingDirectory=/tmp +Capability=CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN +DropCapability=CAP_AUDIT_CONTROL CAP_AUDIT_WRITE +AmbientCapability=CAP_BPF CAP_CHOWN +NoNewPrivileges=no +MachineID=f28f129b51874b1280a89421ec4b4ad4 +PrivateUsers=no +NotifyReady=no +SystemCallFilter=@basic-io @chown +SystemCallFilter=~ @clock +LimitNOFILE=1024:2048 +LimitRTPRIO=8:16 +OOMScoreAdjust=32 +CPUAffinity=0,0-5,1-5 +Hostname=nspawn-settings +ResolvConf=copy-host +Timezone=delete +LinkJournal=no +SuppressSync=no + +[Files] +ReadOnly=no +Volatile=no +TemporaryFileSystem=/tmp +TemporaryFileSystem=/opt/tmp +Inaccessible=/opt/inaccessible +Inaccessible=/opt/also-inaccessible +PrivateUsersOwnership=auto +Overlay=+/var::/var +${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"} + +[Network] +Private=yes +VirtualEthernet=yes +VirtualEthernetExtra=my-fancy-veth1 +VirtualEthernetExtra=fancy-veth2:my-fancy-veth2 +Interface=sd-shared1 sd-shared2:sd-renamed2 sd-shared3:sd-altname3 ${wlan_names:-} +MACVLAN=sd-macvlan1 sd-macvlan2:my-macvlan2 sd-macvlanloong +IPVLAN=sd-ipvlan1 sd-ipvlan2:my-ipvlan2 sd-ipvlanlooong +Zone=sd-zone0 +Port=80 +Port=81:8181 +Port=tcp:60 +Port=udp:60:61 +EOF + cat >"$root/entrypoint.sh" <<\EOF +#!/bin/bash +set -ex + +env + +[[ "$1" == "foo bar" ]] +[[ "$2" == "bar baz" ]] + +[[ "$USER" == root ]] +[[ "$FOO" == bar ]] +[[ "$BAZ" == "hello world" ]] +[[ "$PWD" == /tmp ]] +[[ "$container_uuid" == f28f129b-5187-4b12-80a8-9421ec4b4ad4 ]] +[[ "$(ulimit -S -n)" -eq 1024 ]] +[[ "$(ulimit -H -n)" -eq 2048 ]] +[[ "$(ulimit -S -r)" -eq 8 ]] +[[ "$(ulimit -H -r)" -eq 16 ]] +[[ "$(</proc/self/oom_score_adj)" -eq 32 ]] +[[ "$(hostname)" == nspawn-settings ]] +[[ -e /etc/resolv.conf ]] +[[ ! -e /etc/localtime ]] + +mountpoint /tmp +touch /tmp/foo +mountpoint /opt/tmp +touch /opt/tmp/foo +touch /opt/inaccessible/foo && exit 1 +touch /opt/also-inaccessible/foo && exit 1 +mountpoint /var + +ip link +ip link | grep host-only && exit 1 +ip link | grep host0@ +ip link | grep my-fancy-veth1@ +ip link | grep my-fancy-veth2@ +ip link | grep sd-shared1 +ip link | grep sd-renamed2 +ip link | grep sd-shared3 +ip link | grep sd-altname3 +ip link | grep sd-altname-tooooooooooooo-long +ip link | grep mv-sd-macvlan1@ +ip link | grep my-macvlan2@ +ip link | grep iv-sd-ipvlan1@ +ip link | grep my-ipvlan2@ +EOF + if [[ -n "${wlan_names:-}" ]]; then + cat >>"$root/entrypoint.sh" <<\EOF +ip link | grep wlan0 +ip link | grep wl-renamed1 +EOF + fi + + timeout 30 systemd-nspawn --directory="$root" + + # And now for stuff that needs to run separately + # + # Note on the condition below: since our container tree is owned by root, + # both "yes" and "identity" private users settings will behave the same + # as PrivateUsers=0:65535, which makes BindUser= fail as the UID already + # exists there, so skip setting it in such case + for private_users in "131072:65536" yes identity pick; do + cat >"/run/systemd/nspawn/$container.nspawn" <<EOF +[Exec] +Hostname=private-users +PrivateUsers=$private_users + +[Files] +PrivateUsersOwnership=auto +BindUser= +$([[ "$private_users" =~ (yes|identity) ]] || echo "BindUser=testuser") +${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"} +EOF + cat "/run/systemd/nspawn/$container.nspawn" + chown -R root:root "$root" + systemd-nspawn --directory="$root" bash -xec '[[ "$(hostname)" == private-users ]]' + done + + rm -fr "$root" "/run/systemd/nspawn/$container.nspawn" +} + +bind_user_cleanup() { + userdel --force --remove nspawn-bind-user-1 + userdel --force --remove nspawn-bind-user-2 +} + +testcase_bind_user() { + local root + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.bind-user.XXX)" + create_dummy_container "$root" + useradd --create-home --user-group nspawn-bind-user-1 + useradd --create-home --user-group nspawn-bind-user-2 + trap bind_user_cleanup RETURN + touch /home/nspawn-bind-user-1/foo + touch /home/nspawn-bind-user-2/bar + # Add a couple of POSIX ACLs to test the patch-uid stuff + mkdir -p "$root/opt" + setfacl -R -m 'd:u:nspawn-bind-user-1:rwX' -m 'u:nspawn-bind-user-1:rwX' "$root/opt" + setfacl -R -m 'd:g:nspawn-bind-user-1:rwX' -m 'g:nspawn-bind-user-1:rwX' "$root/opt" + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + bash -xec 'test -e /run/host/home/nspawn-bind-user-1/foo' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --private-users-ownership=chown \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'test -e /run/host/home/nspawn-bind-user-1/foo; test -e /run/host/home/nspawn-bind-user-2/bar' + chown -R root:root "$root" + + # User/group name collision + echo "nspawn-bind-user-2:x:1000:1000:nspawn-bind-user-2:/home/nspawn-bind-user-2:/bin/bash" >"$root/etc/passwd" + (! systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + true) + rm -f "$root/etc/passwd" + + echo "nspawn-bind-user-2:x:1000:" >"$root/etc/group" + (! systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + true) + rm -f "$root/etc/group" + + rm -fr "$root" +} + +testcase_bind_tmp_path() { + # https://github.com/systemd/systemd/issues/4789 + local root + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.bind-tmp-path.XXX)" + create_dummy_container "$root" + : >/tmp/bind + systemd-nspawn --register=no \ + --directory="$root" \ + --bind=/tmp/bind \ + bash -c 'test -e /tmp/bind' + + rm -fr "$root" /tmp/bind +} + +testcase_norbind() { + # https://github.com/systemd/systemd/issues/13170 + local root + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.norbind-path.XXX)" + mkdir -p /tmp/binddir/subdir + echo -n "outer" >/tmp/binddir/subdir/file + mount -t tmpfs tmpfs /tmp/binddir/subdir + echo -n "inner" >/tmp/binddir/subdir/file + create_dummy_container "$root" + + systemd-nspawn --register=no \ + --directory="$root" \ + --bind=/tmp/binddir:/mnt:norbind \ + bash -c 'CONTENT=$(cat /mnt/subdir/file); if [[ $CONTENT != "outer" ]]; then echo "*** unexpected content: $CONTENT"; exit 1; fi' + + umount /tmp/binddir/subdir + rm -fr "$root" /tmp/binddir/ +} + +rootidmap_cleanup() { + local dir="${1:?}" + + mountpoint -q "$dir/bind" && umount "$dir/bind" + rm -fr "$dir" +} + +testcase_rootidmap() { + local root cmd permissions + local owner=1000 + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.rootidmap-path.XXX)" + # Create ext4 image, as ext4 supports idmapped-mounts. + mkdir -p /tmp/rootidmap/bind + dd if=/dev/zero of=/tmp/rootidmap/ext4.img bs=4k count=2048 + mkfs.ext4 /tmp/rootidmap/ext4.img + mount /tmp/rootidmap/ext4.img /tmp/rootidmap/bind + trap "rootidmap_cleanup /tmp/rootidmap/" RETURN + + touch /tmp/rootidmap/bind/file + chown -R "$owner:$owner" /tmp/rootidmap/bind + + create_dummy_container "$root" + cmd='PERMISSIONS=$(stat -c "%u:%g" /mnt/file); if [[ $PERMISSIONS != "0:0" ]]; then echo "*** wrong permissions: $PERMISSIONS"; return 1; fi; touch /mnt/other_file' + if ! SYSTEMD_LOG_TARGET=console \ + systemd-nspawn --register=no \ + --directory="$root" \ + --bind=/tmp/rootidmap/bind:/mnt:rootidmap \ + bash -c "$cmd" |& tee nspawn.out; then + if grep -q "Failed to map ids for bind mount.*: Function not implemented" nspawn.out; then + echo "idmapped mounts are not supported, skipping the test..." + return 0 + fi + + return 1 + fi + + permissions=$(stat -c "%u:%g" /tmp/rootidmap/bind/other_file) + if [[ $permissions != "$owner:$owner" ]]; then + echo "*** wrong permissions: $permissions" + [[ "$IS_USERNS_SUPPORTED" == "yes" ]] && return 1 + fi +} + +owneridmap_cleanup() { + local dir="${1:?}" + + mountpoint -q "$dir/bind" && umount "$dir/bind" + rm -fr "$dir" +} + +testcase_owneridmap() { + local root cmd permissions + local owner=1000 + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.owneridmap-path.XXX)" + # Create ext4 image, as ext4 supports idmapped-mounts. + mkdir -p /tmp/owneridmap/bind + dd if=/dev/zero of=/tmp/owneridmap/ext4.img bs=4k count=2048 + mkfs.ext4 /tmp/owneridmap/ext4.img + mount /tmp/owneridmap/ext4.img /tmp/owneridmap/bind + trap "owneridmap_cleanup /tmp/owneridmap/" RETURN + + touch /tmp/owneridmap/bind/file + chown -R "$owner:$owner" /tmp/owneridmap/bind + + # Allow users to read and execute / in order to execute binaries + chmod o+rx "$root" + + create_dummy_container "$root" + + # --user= + # "Fake" getent passwd's bare minimum, so we don't have to pull it in + # with all the DSO shenanigans + cat >"$root/bin/getent" <<\EOF +#!/bin/bash + +if [[ $# -eq 0 ]]; then + : +elif [[ $1 == passwd ]]; then + echo "testuser:x:1010:1010:testuser:/:/bin/sh" +elif [[ $1 == initgroups ]]; then + echo "testuser" +fi +EOF + chmod +x "$root/bin/getent" + + mkdir -p "$root/home/testuser" + chown 1010:1010 "$root/home/testuser" + + cmd='PERMISSIONS=$(stat -c "%u:%g" /home/testuser/file); if [[ $PERMISSIONS != "1010:1010" ]]; then echo "*** wrong permissions: $PERMISSIONS"; return 1; fi; touch /home/testuser/other_file' + if ! SYSTEMD_LOG_TARGET=console \ + systemd-nspawn --register=no \ + --directory="$root" \ + -U \ + --user=testuser \ + --bind=/tmp/owneridmap/bind:/home/testuser:owneridmap \ + ${COVERAGE_BUILD_DIR:+--bind="$COVERAGE_BUILD_DIR"} \ + /usr/bin/bash -c "$cmd" |& tee nspawn.out; then + if grep -q "Failed to map ids for bind mount.*: Function not implemented" nspawn.out; then + echo "idmapped mounts are not supported, skipping the test..." + return 0 + fi + + return 1 + fi + + permissions=$(stat -c "%u:%g" /tmp/owneridmap/bind/other_file) + if [[ $permissions != "$owner:$owner" ]]; then + echo "*** wrong permissions: $permissions" + [[ "$IS_USERNS_SUPPORTED" == "yes" ]] && return 1 + fi +} + +testcase_notification_socket() { + # https://github.com/systemd/systemd/issues/4944 + local root + local cmd='echo a | nc -U -u -w 1 /run/host/notify' + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.check_notification_socket.XXX)" + create_dummy_container "$root" + + systemd-nspawn --register=no --directory="$root" bash -x -c "$cmd" + systemd-nspawn --register=no --directory="$root" -U bash -x -c "$cmd" + + rm -fr "$root" +} + +testcase_os_release() { + local root entrypoint os_release_source + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.os-release.XXX)" + create_dummy_container "$root" + entrypoint="$root/entrypoint.sh" + cat >"$entrypoint" <<\EOF +#!/usr/bin/bash -ex + +. /tmp/os-release +[[ -n "${ID:-}" && "$ID" != "$container_host_id" ]] && exit 1 +[[ -n "${VERSION_ID:-}" && "$VERSION_ID" != "$container_host_version_id" ]] && exit 1 +[[ -n "${BUILD_ID:-}" && "$BUILD_ID" != "$container_host_build_id" ]] && exit 1 +[[ -n "${VARIANT_ID:-}" && "$VARIANT_ID" != "$container_host_variant_id" ]] && exit 1 + +cd /tmp +(cd /run/host && md5sum os-release) | md5sum -c +EOF + chmod +x "$entrypoint" + + os_release_source="/etc/os-release" + if [[ ! -r "$os_release_source" ]]; then + os_release_source="/usr/lib/os-release" + elif [[ -L "$os_release_source" ]]; then + # Ensure that /etc always wins if available + cp --remove-destination -fv /usr/lib/os-release /etc/os-release + echo MARKER=1 >>/etc/os-release + fi + + systemd-nspawn --register=no \ + --directory="$root" \ + --bind="$os_release_source:/tmp/os-release" \ + "${entrypoint##"$root"}" + + if grep -q MARKER /etc/os-release; then + ln -svrf /usr/lib/os-release /etc/os-release + fi + + rm -fr "$root" +} + +testcase_machinectl_bind() { + local service_path service_name root container_name ec + local cmd='for i in $(seq 1 20); do if test -f /tmp/marker; then exit 0; fi; sleep .5; done; exit 1;' + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.machinectl-bind.XXX)" + create_dummy_container "$root" + container_name="$(basename "$root")" + + service_path="$(mktemp /run/systemd/system/nspawn-machinectl-bind-XXX.service)" + service_name="${service_path##*/}" + cat >"$service_path" <<EOF +[Service] +Type=notify +ExecStart=systemd-nspawn --directory="$root" --notify-ready=no /usr/bin/bash -xec "$cmd" +EOF + + systemctl daemon-reload + systemctl start "$service_name" + touch /tmp/marker + machinectl bind --mkdir "$container_name" /tmp/marker + + timeout 10 bash -c "while [[ '\$(systemctl show -P SubState $service_name)' == running ]]; do sleep .2; done" + ec="$(systemctl show -P ExecMainStatus "$service_name")" + systemctl stop "$service_name" + + rm -fr "$root" "$service_path" + + return "$ec" +} + +testcase_selinux() { + # Basic test coverage to avoid issues like https://github.com/systemd/systemd/issues/19976 + if ! command -v selinuxenabled >/dev/null || ! selinuxenabled; then + echo >&2 "SELinux is not enabled, skipping SELinux-related tests" + return 0 + fi + + local root + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.selinux.XXX)" + create_dummy_container "$root" + chcon -R -t container_t "$root" + + systemd-nspawn --register=no \ + --boot \ + --directory="$root" \ + --selinux-apifs-context=system_u:object_r:container_file_t:s0:c0,c1 \ + --selinux-context=system_u:system_r:container_t:s0:c0,c1 + + rm -fr "$root" +} + +testcase_ephemeral_config() { + # https://github.com/systemd/systemd/issues/13297 + local root container_name + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.ephemeral-config.XXX)" + create_dummy_container "$root" + container_name="$(basename "$root")" + + mkdir -p /run/systemd/nspawn/ + rm -f "/etc/systemd/nspawn/$container_name.nspawn" + cat >"/run/systemd/nspawn/$container_name.nspawn" <<EOF +[Files] +${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"} +BindReadOnly=/tmp/ephemeral-config +EOF + touch /tmp/ephemeral-config + + systemd-nspawn --register=no \ + --directory="$root" \ + --ephemeral \ + bash -x -c "test -f /tmp/ephemeral-config" + + systemd-nspawn --register=no \ + --directory="$root" \ + --ephemeral \ + --machine=foobar \ + bash -x -c "! test -f /tmp/ephemeral-config" + + rm -fr "$root" "/run/systemd/nspawn/$container_name.nspawn" +} + +matrix_run_one() { + local cgroupsv2="${1:?}" + local use_cgns="${2:?}" + local api_vfs_writable="${3:?}" + local root + + if [[ "$cgroupsv2" == "yes" && "$IS_CGROUPSV2_SUPPORTED" == "no" ]]; then + echo >&2 "Unified cgroup hierarchy is not supported, skipping..." + return 0 + fi + + if [[ "$use_cgns" == "yes" && "$IS_CGNS_SUPPORTED" == "no" ]]; then + echo >&2 "CGroup namespaces are not supported, skipping..." + return 0 + fi + + root="$(mktemp -d "/var/lib/machines/TEST-13-NSPAWN.unified-$1-cgns-$2-api-vfs-writable-$3.XXX")" + create_dummy_container "$root" + + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --boot + + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --private-network \ + --boot + + if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --private-users=pick \ + --boot; then + [[ "$IS_USERNS_SUPPORTED" == "yes" && "$api_vfs_writable" == "network" ]] && return 1 + else + [[ "$IS_USERNS_SUPPORTED" == "no" && "$api_vfs_writable" = "network" ]] && return 1 + fi + + if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --private-network \ + --private-users=pick \ + --boot; then + [[ "$IS_USERNS_SUPPORTED" == "yes" && "$api_vfs_writable" == "yes" ]] && return 1 + else + [[ "$IS_USERNS_SUPPORTED" == "no" && "$api_vfs_writable" = "yes" ]] && return 1 + fi + + local netns_opt="--network-namespace-path=/proc/self/ns/net" + local net_opt + local net_opts=( + "--network-bridge=lo" + "--network-interface=lo" + "--network-ipvlan=lo" + "--network-macvlan=lo" + "--network-veth" + "--network-veth-extra=lo" + "--network-zone=zone" + ) + + # --network-namespace-path and network-related options cannot be used together + for net_opt in "${net_opts[@]}"; do + echo "$netns_opt in combination with $net_opt should fail" + if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --boot \ + "$netns_opt" \ + "$net_opt"; then + echo >&2 "unexpected pass" + return 1 + fi + done + + # allow combination of --network-namespace-path and --private-network + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --boot \ + --private-network \ + "$netns_opt" + + # test --network-namespace-path works with a network namespace created by "ip netns" + ip netns add nspawn_test + netns_opt="--network-namespace-path=/run/netns/nspawn_test" + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --network-namespace-path=/run/netns/nspawn_test \ + ip a | grep -v -E '^1: lo.*UP' + ip netns del nspawn_test + + rm -fr "$root" + + return 0 +} + +testcase_check_os_release() { + # https://github.com/systemd/systemd/issues/29185 + local base common_opts root + + base="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.check_os_release_base.XXX)" + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.check_os_release.XXX)" + create_dummy_container "$base" + cp -d "$base"/{bin,sbin,lib,lib64} "$root/" + common_opts=( + --boot + --register=no + --directory="$root" + --bind-ro="$base/usr:/usr" + ) + + # Empty /etc/ & /usr/ + (! systemd-nspawn "${common_opts[@]}") + (! SYSTEMD_NSPAWN_CHECK_OS_RELEASE=1 systemd-nspawn "${common_opts[@]}") + (! SYSTEMD_NSPAWN_CHECK_OS_RELEASE=foo systemd-nspawn "${common_opts[@]}") + SYSTEMD_NSPAWN_CHECK_OS_RELEASE=0 systemd-nspawn "${common_opts[@]}" + + # Empty /usr/ + a broken /etc/os-release -> /usr/os-release symlink + ln -svrf "$root/etc/os-release" "$root/usr/os-release" + (! systemd-nspawn "${common_opts[@]}") + (! SYSTEMD_NSPAWN_CHECK_OS_RELEASE=1 systemd-nspawn "${common_opts[@]}") + SYSTEMD_NSPAWN_CHECK_OS_RELEASE=0 systemd-nspawn "${common_opts[@]}" + + rm -fr "$root" "$base" +} + +run_testcases + +for api_vfs_writable in yes no network; do + matrix_run_one no no $api_vfs_writable + matrix_run_one yes no $api_vfs_writable + matrix_run_one no yes $api_vfs_writable + matrix_run_one yes yes $api_vfs_writable +done |