summaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/README.testsuite4
-rwxr-xr-xtest/TEST-64-UDEV-STORAGE/long_sysfs_path.configure2
-rwxr-xr-xtest/TEST-64-UDEV-STORAGE/test.sh2
-rw-r--r--test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.service6
-rw-r--r--test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.timer10
-rw-r--r--test/TEST-74-AUX-UTILS/meson.build3
-rw-r--r--test/TEST-86-MULTI-PROFILE-UKI/Makefile6
-rw-r--r--test/TEST-86-MULTI-PROFILE-UKI/meson.build11
-rw-r--r--test/fuzz/fuzz-network-parser/oss-fuzz-372994449bin0 -> 4740 bytes
-rw-r--r--test/fuzz/fuzz-unit-file/directives-all.service1
-rw-r--r--test/fuzz/fuzz-unit-file/tmpfiles-clean.timer1
-rwxr-xr-xtest/integration-test-wrapper.py15
-rw-r--r--test/meson.build1
-rwxr-xr-xtest/test-compare-versions.sh44
-rwxr-xr-xtest/test-fstab-generator.sh4
-rw-r--r--test/test-functions13
-rwxr-xr-xtest/test-network/systemd-networkd-tests.py32
-rwxr-xr-xtest/test-sysusers.sh.in36
-rwxr-xr-xtest/test-udev.py11
-rwxr-xr-xtest/units/TEST-13-NSPAWN.machined.sh (renamed from test/units/TEST-13-NSPAWN.machinectl.sh)101
-rwxr-xr-xtest/units/TEST-17-UDEV.11.sh4
-rwxr-xr-xtest/units/TEST-17-UDEV.database.sh22
-rwxr-xr-xtest/units/TEST-17-UDEV.diskseq.sh71
-rwxr-xr-xtest/units/TEST-19-CGROUP.keyed-properties.sh65
-rwxr-xr-xtest/units/TEST-22-TMPFILES.18.sh17
-rwxr-xr-xtest/units/TEST-23-UNIT-FILE-ExtraFileDescriptors-child.sh14
-rwxr-xr-xtest/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh65
-rwxr-xr-xtest/units/TEST-29-PORTABLE.directory.sh152
-rwxr-xr-xtest/units/TEST-29-PORTABLE.image.sh240
-rwxr-xr-xtest/units/TEST-29-PORTABLE.sh386
-rwxr-xr-xtest/units/TEST-50-DISSECT.dissect.sh18
-rwxr-xr-xtest/units/TEST-54-CREDS.sh4
-rwxr-xr-xtest/units/TEST-58-REPART.sh204
-rwxr-xr-xtest/units/TEST-65-ANALYZE.sh37
-rwxr-xr-xtest/units/TEST-67-INTEGRITY.sh2
-rwxr-xr-xtest/units/TEST-74-AUX-UTILS.busctl.sh8
-rwxr-xr-xtest/units/TEST-74-AUX-UTILS.defer_reactivation.sh23
-rwxr-xr-xtest/units/TEST-74-AUX-UTILS.networkctl.sh30
-rwxr-xr-xtest/units/TEST-74-AUX-UTILS.run.sh12
-rwxr-xr-xtest/units/TEST-86-MULTI-PROFILE-UKI.sh81
40 files changed, 1299 insertions, 459 deletions
diff --git a/test/README.testsuite b/test/README.testsuite
index 60dc03498b..8cacbb40af 100644
--- a/test/README.testsuite
+++ b/test/README.testsuite
@@ -202,10 +202,6 @@ systemd-nspawn.
`TEST_NO_KVM=1`: Disable qemu KVM auto-detection (may be necessary when you're
trying to run the *vanilla* qemu and have both qemu and qemu-kvm installed)
-`TEST_NESTED_KVM=1`: Allow tests to run with nested KVM. By default, the
-testsuite disables nested KVM if the host machine already runs under KVM.
-Setting this variable disables such checks.
-
`QEMU_MEM=512M`: Configure amount of memory for qemu VMs (defaults to 512M).
`QEMU_SMP=1`: Configure number of CPUs for qemu VMs (defaults to 1).
diff --git a/test/TEST-64-UDEV-STORAGE/long_sysfs_path.configure b/test/TEST-64-UDEV-STORAGE/long_sysfs_path.configure
index 6e8e3124ba..f29677ce66 100755
--- a/test/TEST-64-UDEV-STORAGE/long_sysfs_path.configure
+++ b/test/TEST-64-UDEV-STORAGE/long_sysfs_path.configure
@@ -26,6 +26,6 @@ for bridge in range(1, 26):
f"pci-bridge,id=pci_bridge{bridge},bus=pci_bridge{bridge - 1},chassis_nr={64 + bridge},addr=1",
]
-config["QemuArgs"] += ["-device", f"virtio-blk-pci,drive=drive0,scsi=off,bus=pci_bridge25,addr=1"]
+config["QemuArgs"] += ["-device", f"virtio-blk-pci,drive=drive0,bus=pci_bridge25,addr=1"]
json.dump(config, sys.stdout)
diff --git a/test/TEST-64-UDEV-STORAGE/test.sh b/test/TEST-64-UDEV-STORAGE/test.sh
index b5a70cafaf..2fd7bf2bfb 100755
--- a/test/TEST-64-UDEV-STORAGE/test.sh
+++ b/test/TEST-64-UDEV-STORAGE/test.sh
@@ -464,7 +464,7 @@ testcase_long_sysfs_path() {
qemu_opts+=("-device pci-bridge,id=pci_bridge$brid,bus=pci_bridge$((brid-1)),chassis_nr=$((64+brid))")
done
- qemu_opts+=("-device virtio-blk-pci,drive=drive0,scsi=off,bus=pci_bridge$brid")
+ qemu_opts+=("-device virtio-blk-pci,drive=drive0,bus=pci_bridge$brid")
KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}"
QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}"
diff --git a/test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.service b/test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.service
new file mode 100644
index 0000000000..a754626828
--- /dev/null
+++ b/test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Testing systemd timers
+
+[Service]
+Type=simple
+ExecStart=sh -c 'date +%%s >>/tmp/realtime-test.log ; sleep 5'
diff --git a/test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.timer b/test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.timer
new file mode 100644
index 0000000000..b870b41628
--- /dev/null
+++ b/test/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/realtime-test.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Testing systemd timers
+
+[Timer]
+OnCalendar=*:*:0/5
+AccuracySec=1us
+DeferReactivation=true
+
+[Install]
+WantedBy=timers.target
diff --git a/test/TEST-74-AUX-UTILS/meson.build b/test/TEST-74-AUX-UTILS/meson.build
index 43a733ee64..543eee195f 100644
--- a/test/TEST-74-AUX-UTILS/meson.build
+++ b/test/TEST-74-AUX-UTILS/meson.build
@@ -4,5 +4,8 @@ integration_tests += [
integration_test_template + {
'name' : fs.name(meson.current_source_dir()),
'storage': 'persistent',
+ 'vm' : true,
},
]
+
+testdata_subdirs += [meson.current_source_dir() / 'TEST-74-AUX-UTILS.units']
diff --git a/test/TEST-86-MULTI-PROFILE-UKI/Makefile b/test/TEST-86-MULTI-PROFILE-UKI/Makefile
new file mode 100644
index 0000000000..653f16163f
--- /dev/null
+++ b/test/TEST-86-MULTI-PROFILE-UKI/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+all setup run clean clean-again:
+ true
+
+.PHONY: all setup run clean clean-again
diff --git a/test/TEST-86-MULTI-PROFILE-UKI/meson.build b/test/TEST-86-MULTI-PROFILE-UKI/meson.build
new file mode 100644
index 0000000000..10d5957d8f
--- /dev/null
+++ b/test/TEST-86-MULTI-PROFILE-UKI/meson.build
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+integration_tests += [
+ integration_test_template + {
+ 'name' : fs.name(meson.current_source_dir()),
+ 'storage' : 'persistent',
+ 'vm' : true,
+ 'firmware' : 'auto',
+ 'enabled' : false,
+ },
+]
diff --git a/test/fuzz/fuzz-network-parser/oss-fuzz-372994449 b/test/fuzz/fuzz-network-parser/oss-fuzz-372994449
new file mode 100644
index 0000000000..76c3a4903c
--- /dev/null
+++ b/test/fuzz/fuzz-network-parser/oss-fuzz-372994449
Binary files differ
diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service
index dbd56ec752..1cb212bcad 100644
--- a/test/fuzz/fuzz-unit-file/directives-all.service
+++ b/test/fuzz/fuzz-unit-file/directives-all.service
@@ -7,6 +7,7 @@ AllowedCPUs=
AllowedMemoryNodes=
AllowIsolate=
Also=
+DeferReactivation=
AmbientCapabilities=
AssertACPower=
AssertArchitecture=
diff --git a/test/fuzz/fuzz-unit-file/tmpfiles-clean.timer b/test/fuzz/fuzz-unit-file/tmpfiles-clean.timer
index 5bf91b9f4c..5dc269243f 100644
--- a/test/fuzz/fuzz-unit-file/tmpfiles-clean.timer
+++ b/test/fuzz/fuzz-unit-file/tmpfiles-clean.timer
@@ -33,6 +33,7 @@ Persistent=true
AccuracySec=24h
RandomizedDelaySec=234234234
FixedRandomDelay=true
+DeferReactivation=true
Persistent=no
Unit=foo.service
diff --git a/test/integration-test-wrapper.py b/test/integration-test-wrapper.py
index a8004b3815..a680ddee53 100755
--- a/test/integration-test-wrapper.py
+++ b/test/integration-test-wrapper.py
@@ -61,9 +61,10 @@ def main():
print(f"TEST_NO_QEMU=1, skipping {args.name}", file=sys.stderr)
exit(77)
- if args.name in os.getenv("TEST_SKIP", "").split():
- print(f"Skipping {args.name} due to TEST_SKIP", file=sys.stderr)
- exit(77)
+ for s in os.getenv("TEST_SKIP", "").split():
+ if s in args.name:
+ print(f"Skipping {args.name} due to TEST_SKIP", file=sys.stderr)
+ exit(77)
keep_journal = os.getenv("TEST_SAVE_JOURNAL", "fail")
shell = bool(int(os.getenv("TEST_SHELL", "0")))
@@ -174,6 +175,14 @@ def main():
result = subprocess.run(cmd)
+ # On Debian/Ubuntu we get a lot of random QEMU crashes. Retry once, and then skip if it fails again.
+ if args.vm and result.returncode == 247 and args.exit_code != 247:
+ journal_file.unlink(missing_ok=True)
+ result = subprocess.run(cmd)
+ if args.vm and result.returncode == 247 and args.exit_code != 247:
+ print(f"Test {args.name} failed due to QEMU crash (error 247), ignoring", file=sys.stderr)
+ exit(77)
+
if journal_file and (keep_journal == "0" or (result.returncode in (args.exit_code, 77) and keep_journal == "fail")):
journal_file.unlink(missing_ok=True)
diff --git a/test/meson.build b/test/meson.build
index 6acff37508..9d1a069fc9 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -376,6 +376,7 @@ foreach dirname : [
'TEST-83-BTRFS',
'TEST-84-STORAGETM',
'TEST-85-NETWORK',
+ 'TEST-86-MULTI-PROFILE-UKI',
]
subdir(dirname)
endforeach
diff --git a/test/test-compare-versions.sh b/test/test-compare-versions.sh
index c40208be60..e9acd9cbee 100755
--- a/test/test-compare-versions.sh
+++ b/test/test-compare-versions.sh
@@ -4,32 +4,32 @@ set -e
ANALYZE="${1:-systemd-analyze}"
-$ANALYZE compare-versions 1 lt 2
-$ANALYZE compare-versions 1 '<' 2
-$ANALYZE compare-versions 1 le 2
-$ANALYZE compare-versions 1 '<=' 2
-$ANALYZE compare-versions 1 ne 2
-$ANALYZE compare-versions 1 '!=' 2
-( ! $ANALYZE compare-versions 1 ge 2 )
-( ! $ANALYZE compare-versions 1 '>=' 2 )
-( ! $ANALYZE compare-versions 1 eq 2 )
-( ! $ANALYZE compare-versions 1 '==' 2 )
-( ! $ANALYZE compare-versions 1 gt 2 )
-( ! $ANALYZE compare-versions 1 '>' 2 )
+"$ANALYZE" compare-versions 1 lt 2
+"$ANALYZE" compare-versions 1 '<' 2
+"$ANALYZE" compare-versions 1 le 2
+"$ANALYZE" compare-versions 1 '<=' 2
+"$ANALYZE" compare-versions 1 ne 2
+"$ANALYZE" compare-versions 1 '!=' 2
+( ! "$ANALYZE" compare-versions 1 ge 2 )
+( ! "$ANALYZE" compare-versions 1 '>=' 2 )
+( ! "$ANALYZE" compare-versions 1 eq 2 )
+( ! "$ANALYZE" compare-versions 1 '==' 2 )
+( ! "$ANALYZE" compare-versions 1 gt 2 )
+( ! "$ANALYZE" compare-versions 1 '>' 2 )
-test "$($ANALYZE compare-versions 1 2)" = '1 < 2'
-test "$($ANALYZE compare-versions 2 2)" = '2 == 2'
-test "$($ANALYZE compare-versions 2 1)" = '2 > 1'
-test "$($ANALYZE compare-versions '' '')" = "'' == ''"
+test "$("$ANALYZE" compare-versions 1 2)" = '1 < 2'
+test "$("$ANALYZE" compare-versions 2 2)" = '2 == 2'
+test "$("$ANALYZE" compare-versions 2 1)" = '2 > 1'
+test "$("$ANALYZE" compare-versions '' '')" = "'' == ''"
set +e
-$ANALYZE compare-versions 1 2; ret1=$?
-$ANALYZE compare-versions 2 2; ret2=$?
-$ANALYZE compare-versions 2 1; ret3=$?
+"$ANALYZE" compare-versions 1 2; ret1=$?
+"$ANALYZE" compare-versions 2 2; ret2=$?
+"$ANALYZE" compare-versions 2 1; ret3=$?
set -e
-test $ret1 == 12
-test $ret2 == 0
-test $ret3 == 11
+test "$ret1" == 12
+test "$ret2" == 0
+test "$ret3" == 11
diff --git a/test/test-fstab-generator.sh b/test/test-fstab-generator.sh
index af8fa7c226..24a2533f16 100755
--- a/test/test-fstab-generator.sh
+++ b/test/test-fstab-generator.sh
@@ -44,9 +44,9 @@ test_one() (
fi
if [[ "${input##*/}" =~ \.fstab\.input ]]; then
- SYSTEMD_LOG_LEVEL=debug SYSTEMD_IN_INITRD="$initrd" SYSTEMD_SYSFS_CHECK=no SYSTEMD_PROC_CMDLINE="fstab=yes root=fstab" SYSTEMD_FSTAB="$input" SYSTEMD_SYSROOT_FSTAB="/dev/null" $generator "$out" "$out" "$out"
+ SYSTEMD_LOG_LEVEL=debug SYSTEMD_IN_INITRD="$initrd" SYSTEMD_SYSFS_CHECK=no SYSTEMD_PROC_CMDLINE="fstab=yes root=fstab" SYSTEMD_FSTAB="$input" SYSTEMD_SYSROOT_FSTAB="/dev/null" "$generator" "$out" "$out" "$out"
else
- SYSTEMD_LOG_LEVEL=debug SYSTEMD_IN_INITRD="$initrd" SYSTEMD_SYSFS_CHECK=no SYSTEMD_PROC_CMDLINE="fstab=no $(cat "$input")" $generator "$out" "$out" "$out"
+ SYSTEMD_LOG_LEVEL=debug SYSTEMD_IN_INITRD="$initrd" SYSTEMD_SYSFS_CHECK=no SYSTEMD_PROC_CMDLINE="fstab=no $(cat "$input")" "$generator" "$out" "$out" "$out"
fi
# The option x-systemd.growfs creates symlink to system's systemd-growfs@.service in .mount.wants directory.
diff --git a/test/test-functions b/test/test-functions
index 64a664b69e..8a4cc55626 100644
--- a/test/test-functions
+++ b/test/test-functions
@@ -84,14 +84,12 @@ add_at_exit_handler() {
}
# Decide if we can (and want to) run qemu with KVM acceleration.
-# Check if nested KVM is explicitly enabled (TEST_NESTED_KVM). If not,
-# check if it's not explicitly disabled (TEST_NO_KVM) and we're not already
-# running under KVM. If these conditions are met, enable KVM (and possibly
-# nested KVM), otherwise disable it.
-if get_bool "${TEST_NESTED_KVM:=}" || (! get_bool "${TEST_NO_KVM:=}" && ! systemd-detect-virt -qv); then
- QEMU_KVM=yes
-else
+# Check if KVM is not explicitly disabled (TEST_NO_KVM), otherwise
+# enable KVM (and possibly nested KVM).
+if get_bool "${TEST_NO_KVM:=}"; then
QEMU_KVM=no
+else
+ QEMU_KVM=yes
fi
if ! ROOTLIBDIR=$(pkg-config --variable=systemdutildir systemd); then
@@ -213,6 +211,7 @@ BASICTOOLS=(
ping
pkill
ps
+ pwd
readlink
realpath
rev
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index a596537ffa..687fda7b3d 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -8034,6 +8034,38 @@ class NetworkdMTUTests(unittest.TestCase, Utilities):
copy_network_unit('12-dummy.netdev', '12-dummy-mtu.link', '12-dummy.network.d/ipv6-mtu-1550.conf')
self.check_mtu('1600', '1550', reset=False)
+class NetworkdSysctlTest(unittest.TestCase, Utilities):
+
+ def setUp(self):
+ setup_common()
+
+ def tearDown(self):
+ tear_down_common()
+
+ @unittest.skipUnless(compare_kernel_version("6.12"), reason="On kernels <= 6.12, bpf_current_task_under_cgroup() isn't available for program types BPF_PROG_TYPE_CGROUP_SYSCTL")
+ def check_sysctl_watch(self):
+ copy_network_unit('12-dummy.network', '12-dummy.netdev', '12-dummy.link')
+ start_networkd()
+
+ self.wait_online('dummy98:routable')
+
+ # Change managed sysctls
+ call('sysctl -w net.ipv6.conf.dummy98.accept_ra=1')
+ call('sysctl -w net.ipv6.conf.dummy98.mtu=1360')
+ call('sysctl -w net.ipv4.conf.dummy98.promote_secondaries=0')
+ call('sysctl -w net.ipv6.conf.dummy98.proxy_ndp=1')
+
+ # And unmanaged ones
+ call('sysctl -w net.ipv6.conf.dummy98.hop_limit=4')
+ call('sysctl -w net.ipv6.conf.dummy98.max_addresses=10')
+
+ log=read_networkd_log()
+ self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv6/conf/dummy98/accept_ra' from '0' to '1', conflicting with our setting to '0'")
+ self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv6/conf/dummy98/mtu' from '1550' to '1360', conflicting with our setting to '1550'")
+ self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv4/conf/dummy98/promote_secondaries' from '1' to '0', conflicting with our setting to '1'")
+ self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv6/conf/dummy98/proxy_ndp' from '0' to '1', conflicting with our setting to '0'")
+ self.assertNotIn("changed sysctl '/proc/sys/net/ipv6/conf/dummy98/hop_limit'", log)
+ self.assertNotIn("changed sysctl '/proc/sys/net/ipv6/conf/dummy98/max_addresses'", log)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
diff --git a/test/test-sysusers.sh.in b/test/test-sysusers.sh.in
index 9af253f6c7..fdeae2c898 100755
--- a/test/test-sysusers.sh.in
+++ b/test/test-sysusers.sh.in
@@ -53,7 +53,7 @@ for f in $(find "$SOURCE"/test-*.input | sort -V); do
echo "*** Running $f"
prepare_testdir "${f%.input}"
cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
- $SYSUSERS --root="$TESTDIR"
+ "$SYSUSERS" --root="$TESTDIR"
compare "${f%.*}" ""
done
@@ -62,7 +62,7 @@ for f in $(find "$SOURCE"/test-*.input | sort -V); do
echo "*** Running $f on stdin"
prepare_testdir "${f%.input}"
touch "$TESTDIR/etc/sysusers.d/test.conf"
- $SYSUSERS --root="$TESTDIR" - <"$f"
+ "$SYSUSERS" --root="$TESTDIR" - <"$f"
compare "${f%.*}" "on stdin"
done
@@ -72,9 +72,9 @@ for f in $(find "$SOURCE"/test-*.input | sort -V); do
prepare_testdir "${f%.input}"
touch "$TESTDIR/etc/sysusers.d/test.conf"
# this overrides test.conf which is masked on disk
- $SYSUSERS --root="$TESTDIR" --replace=/etc/sysusers.d/test.conf - <"$f"
+ "$SYSUSERS" --root="$TESTDIR" --replace=/etc/sysusers.d/test.conf - <"$f"
# this should be ignored
- $SYSUSERS --root="$TESTDIR" --replace=/usr/lib/sysusers.d/test.conf - <"$SOURCE/test-1.input"
+ "$SYSUSERS" --root="$TESTDIR" --replace=/usr/lib/sysusers.d/test.conf - <"$SOURCE/test-1.input"
compare "${f%.*}" "on stdin with --replace"
done
@@ -84,9 +84,9 @@ echo "*** Testing --inline"
prepare_testdir "$SOURCE/inline"
# copy a random file to make sure it is ignored
cp "$f" "$TESTDIR/etc/sysusers.d/confuse.conf"
-$SYSUSERS --root="$TESTDIR" --inline \
- "u u1 222 - - /bin/zsh" \
- "g g1 111"
+"$SYSUSERS" --root="$TESTDIR" --inline \
+ "u u1 222 - - /bin/zsh" \
+ "g g1 111"
compare "$SOURCE/inline" "(--inline)"
@@ -95,19 +95,19 @@ echo "*** Testing --inline with --replace"
prepare_testdir "$SOURCE/inline"
# copy a random file to make sure it is ignored
cp "$f" "$TESTDIR/etc/sysusers.d/confuse.conf"
-$SYSUSERS --root="$TESTDIR" \
- --inline \
- --replace=/etc/sysusers.d/confuse.conf \
- "u u1 222 - - /bin/zsh" \
- "g g1 111"
+"$SYSUSERS" --root="$TESTDIR" \
+ --inline \
+ --replace=/etc/sysusers.d/confuse.conf \
+ "u u1 222 - - /bin/zsh" \
+ "g g1 111"
compare "$SOURCE/inline" "(--inline --replace=…)"
echo "*** Testing --inline with no /etc"
rm -rf "${TESTDIR:?}/etc"
-$SYSUSERS --root="$TESTDIR" --inline \
- "u u1 222 - - /bin/zsh" \
- "g g1 111"
+"$SYSUSERS" --root="$TESTDIR" --inline \
+ "u u1 222 - - /bin/zsh" \
+ "g g1 111"
compare "$SOURCE/inline" "(--inline)"
@@ -136,7 +136,7 @@ for f in $(find "$SOURCE"/test-*.input | sort -V); do
echo "*** Running $f (with login.defs)"
prepare_testdir "${f%.input}"
cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
- $SYSUSERS --root="$TESTDIR"
+ "$SYSUSERS" --root="$TESTDIR"
# shellcheck disable=SC2050
[ @ENABLE_COMPAT_MUTABLE_UID_BOUNDARIES@ = 1 ] && bound=555 || bound=$system_guid_max
@@ -152,7 +152,7 @@ for f in $(find "$SOURCE"/test-*.input | sort -V); do
echo "*** Running $f (with login.defs symlinked)"
prepare_testdir "${f%.input}"
cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
- $SYSUSERS --root="$TESTDIR"
+ "$SYSUSERS" --root="$TESTDIR"
# shellcheck disable=SC2050
[ @ENABLE_COMPAT_MUTABLE_UID_BOUNDARIES@ = 1 ] && bound=555 || bound=$system_guid_max
@@ -166,7 +166,7 @@ for f in $(find "$SOURCE"/unhappy-*.input | sort -V); do
echo "*** Running test $f"
prepare_testdir "${f%.input}"
cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
- SYSTEMD_LOG_LEVEL=info $SYSUSERS --root="$TESTDIR" 2>&1 | tail -n1 | sed -r 's/^[^:]+:[^:]+://' >"$TESTDIR/err"
+ SYSTEMD_LOG_LEVEL=info "$SYSUSERS" --root="$TESTDIR" 2>&1 | tail -n1 | sed -r 's/^[^:]+:[^:]+://' >"$TESTDIR/err"
if ! diff -u "$TESTDIR/err" "${f%.*}.expected-err"; then
echo >&2 "**** Unexpected error output for $f"
cat >&2 "$TESTDIR/err"
diff --git a/test/test-udev.py b/test/test-udev.py
index d9d840eb8c..68c48fd790 100755
--- a/test/test-udev.py
+++ b/test/test-udev.py
@@ -2313,6 +2313,17 @@ SUBSYSTEMS=="scsi", PROGRAM=="/bin/sh -c \"printf %%s 'foo1 foo2' | grep 'foo1 f
SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNEL=="sd*", SYMLINK+="blockdev"
KERNEL=="sda6", OPTIONS+="link_priority=10"
"""),
+
+ Rules.new(
+ "case insensitive match",
+ Device(
+ "/devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda/sda1",
+ exp_links = ["ok"],
+ ),
+
+ rules = r"""
+ KERNEL==i"SDA1", SUBSYSTEMS==i"SCSI", ATTRS{vendor}==i"a?a", SYMLINK+="ok"
+ """),
]
def fork_and_run_udev(action: str, rules: Rules) -> None:
diff --git a/test/units/TEST-13-NSPAWN.machinectl.sh b/test/units/TEST-13-NSPAWN.machined.sh
index 7ff953bae9..da62b465ea 100755
--- a/test/units/TEST-13-NSPAWN.machinectl.sh
+++ b/test/units/TEST-13-NSPAWN.machined.sh
@@ -12,7 +12,7 @@ export PAGER=
at_exit() {
set +e
- machinectl status long-running >/dev/null && machinectl kill --signal=KILL long-running
+ machinectl status long-running &>/dev/null && machinectl kill --signal=KILL long-running
mountpoint -q /var/lib/machines && timeout 10 sh -c "until umount /var/lib/machines; do sleep .5; done"
[[ -n "${NSPAWN_FRAGMENT:-}" ]] && rm -f "/etc/systemd/nspawn/$NSPAWN_FRAGMENT" "/var/lib/machines/$NSPAWN_FRAGMENT"
rm -f /run/systemd/nspawn/*.nspawn
@@ -39,6 +39,7 @@ cat >/var/lib/machines/long-running/sbin/init <<\EOF
PID=0
+trap "touch /terminate; kill $PID" RTMIN+3
trap "touch /poweroff" RTMIN+4
trap "touch /reboot" INT
trap "touch /trap" TRAP
@@ -47,12 +48,31 @@ trap 'kill $PID' EXIT
# We need to wait for the sleep process asynchronously in order to allow
# bash to process signals
sleep infinity &
+
+# notify that the process is ready
+touch /ready
+
PID=$!
while :; do
wait || :
done
EOF
-machinectl start long-running
+
+long_running_machine_start() {
+ # shellcheck disable=SC2015
+ machinectl status long-running &>/dev/null && return 0 || true
+
+ # Ensure the service stopped.
+ systemctl stop systemd-nspawn@long-running.service 2>/dev/null || :
+
+ rm -f /var/lib/machines/long-running/ready
+ machinectl start long-running
+ # !!!! DO NOT REMOVE THIS TEST
+ # The test makes sure that the long-running's init script has enough time to start and registered signal traps
+ timeout 30 bash -c "until test -e /var/lib/machines/long-running/ready; do sleep .5; done"
+}
+
+long_running_machine_start
machinectl
machinectl --no-pager --help
@@ -98,7 +118,14 @@ timeout 10 bash -c "until test -e /var/lib/machines/long-running/poweroff; do sl
rm -f /var/lib/machines/long-running/reboot
machinectl reboot long-running
timeout 10 bash -c "until test -e /var/lib/machines/long-running/reboot; do sleep .5; done"
-# Skip machinectl terminate for now, as it doesn't play well with our "init"
+# Test for 'machinectl terminate'
+rm -f /var/lib/machines/long-running/terminate
+machinectl terminate long-running
+timeout 10 bash -c "until test -e /var/lib/machines/long-running/terminate; do sleep .5; done"
+timeout 10 bash -c "while machinectl status long-running &>/dev/null; do sleep .5; done"
+# Restart container
+long_running_machine_start
+# Test for 'machinectl kill'
rm -f /var/lib/machines/long-running/trap
machinectl kill --signal=SIGTRAP --kill-whom=leader long-running
timeout 10 bash -c "until test -e /var/lib/machines/long-running/trap; do sleep .5; done"
@@ -223,5 +250,69 @@ done
(! machinectl read-only container1 foo)
(! machinectl read-only container1 -- -1)
-varlinkctl --more call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{}'
-varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":".host"}'
+####################
+# varlinkctl tests #
+# ##################
+
+long_running_machine_start
+
+varlinkctl introspect /run/systemd/machine/io.systemd.Machine io.systemd.Machine
+varlinkctl introspect /run/systemd/machine/io.systemd.Machine io.systemd.MachineImage
+varlinkctl introspect /run/systemd/machine/io.systemd.MachineImage io.systemd.Machine
+varlinkctl introspect /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage
+
+# test io.systemd.Machine.List
+varlinkctl --more call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{}' | grep 'long-running'
+varlinkctl --more call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{}' | grep '.host'
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":"long-running"}'
+
+pid=$(varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":"long-running"}' | jq '.leader')
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":"long-running"}' >/tmp/expected
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List "{\"pid\":$pid}" | diff /tmp/expected -
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List "{\"name\":\"long-running\", \"pid\":$pid}" | diff /tmp/expected -
+(! varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List "{\"name\":\"non-existent\", \"pid\":$pid}")
+(! varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":""}')
+(! varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":"ah@??.hmm"}')
+
+# test io.systemd.Machine.Kill
+# sending TRAP signal
+rm -f /var/lib/machines/long-running/trap
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Kill '{"name":"long-running", "whom": "leader", "signal": 5}'
+timeout 30 bash -c "until test -e /var/lib/machines/long-running/trap; do sleep .5; done"
+
+# test io.systemd.Machine.Terminate
+long_running_machine_start
+rm -f /var/lib/machines/long-running/terminate
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Terminate '{"name":"long-running"}'
+timeout 10 bash -c "until test -e /var/lib/machines/long-running/terminate; do sleep .5; done"
+timeout 30 bash -c "while varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{\"name\":\"long-running\"}'; do sleep 0.5; done"
+
+# test io.systemd.Machine.Register
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Register '{"name": "registered-container", "class": "container"}'
+timeout 30 bash -c "until varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{\"name\":\"registered-container\"}'; do sleep 0.5; done"
+
+# test io.systemd.Machine.Unregister
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Unregister '{"name": "registered-container"}'
+timeout 30 bash -c "while varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{\"name\":\"registered-container\"}'; do sleep 0.5; done"
+
+# test io.systemd.Machine.List with sshAddress and sshPrivateKeyPath fields
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Register '{"name": "registered-container", "class": "container", "sshAddress": "localhost", "sshPrivateKeyPath": "/non-existent"}'
+timeout 30 bash -c "until varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{\"name\":\"registered-container\"}'; do sleep 0.5; done"
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":"registered-container"}' | jq '.sshAddress' | grep -q 'localhost'
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List '{"name":"registered-container"}' | jq '.sshPrivateKeyPath' | grep -q 'non-existent'
+varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Unregister '{"name": "registered-container"}'
+
+# test io.systemd.MachineImage.List
+varlinkctl --more call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{}' | grep 'long-running'
+varlinkctl --more call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{}' | grep '.host'
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{"name":"long-running"}'
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{"name":"long-running", "acquireMetadata": true}' | grep 'OSRelease'
+
+# test io.systemd.MachineImage.Update
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.Update '{"name":"long-running", "newName": "long-running-renamed", "readOnly": true}'
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{"name":"long-running-renamed"}'
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{"name":"long-running-renamed"}' | jq '.readOnly' | grep 'true'
+
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.Update '{"name":"long-running-renamed", "newName": "long-running", "readOnly": false}'
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{"name":"long-running"}'
+varlinkctl call /run/systemd/machine/io.systemd.MachineImage io.systemd.MachineImage.List '{"name":"long-running"}' | jq '.readOnly' | grep 'false'
diff --git a/test/units/TEST-17-UDEV.11.sh b/test/units/TEST-17-UDEV.11.sh
index 42b925f60b..8413d3c189 100755
--- a/test/units/TEST-17-UDEV.11.sh
+++ b/test/units/TEST-17-UDEV.11.sh
@@ -237,6 +237,8 @@ test_syntax_error 'ENV=="b"' 'Invalid attribute for ENV.'
test_syntax_error 'ENV{a}-="b"' 'Invalid operator for ENV.'
test_syntax_error 'ENV{a}:="b"' "ENV key takes '==', '!=', '=', or '+=' operator, assuming '='."
test_syntax_error 'ENV{ACTION}="b"' "Invalid ENV attribute. 'ACTION' cannot be set."
+test_syntax_error 'ENV{a}=i"b"' "Invalid prefix 'i' for 'ENV'. The 'i' prefix can be specified only for '==' or '!=' operator."
+test_syntax_error 'ENV{a}+=i"b"' "Invalid prefix 'i' for 'ENV'. The 'i' prefix can be specified only for '==' or '!=' operator."
test_syntax_error 'CONST=="b"' 'Invalid attribute for CONST.'
test_syntax_error 'CONST{a}=="b"' 'Invalid attribute for CONST.'
test_syntax_error 'CONST{arch}="b"' 'Invalid operator for CONST.'
@@ -275,10 +277,12 @@ test_syntax_error 'TEST{0644}="b"' 'Invalid operator for TEST.'
test_syntax_error 'PROGRAM{a}=="b"' 'Invalid attribute for PROGRAM.'
test_syntax_error 'PROGRAM-="b"' 'Invalid operator for PROGRAM.'
test_syntax_error 'PROGRAM=="%", NAME="b"' 'Invalid value "%" for PROGRAM (char 1: invalid substitution type), ignoring.'
+test_syntax_error 'PROGRAM==i"b"' "Invalid prefix 'i' for PROGRAM."
test_syntax_error 'IMPORT="b"' 'Invalid attribute for IMPORT.'
test_syntax_error 'IMPORT{a}="b"' 'Invalid attribute for IMPORT.'
test_syntax_error 'IMPORT{a}-="b"' 'Invalid operator for IMPORT.'
test_syntax_error 'IMPORT{file}=="%", NAME="b"' 'Invalid value "%" for IMPORT (char 1: invalid substitution type), ignoring.'
+test_syntax_error 'IMPORT{file}==i"a", NAME="b"' "Invalid prefix 'i' for IMPORT."
test_syntax_error 'IMPORT{builtin}!="foo"' 'Unknown builtin command: foo'
test_syntax_error 'RESULT{a}=="b"' 'Invalid attribute for RESULT.'
test_syntax_error 'RESULT:="b"' 'Invalid operator for RESULT.'
diff --git a/test/units/TEST-17-UDEV.database.sh b/test/units/TEST-17-UDEV.database.sh
new file mode 100755
index 0000000000..2b66333cad
--- /dev/null
+++ b/test/units/TEST-17-UDEV.database.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -ex
+set -o pipefail
+
+udevadm control --log-level=debug
+
+IFNAME=test-udev-aaa
+ip link add "$IFNAME" type dummy
+IFINDEX=$(ip -json link show "$IFNAME" | jq '.[].ifindex')
+udevadm wait --timeout 10 "/sys/class/net/$IFNAME"
+# Check if the database file is created.
+[[ -e "/run/udev/data/n$IFINDEX" ]]
+
+ip link del "$IFNAME"
+udevadm wait --timeout 10 --removed --settle "/sys/class/net/$IFNAME"
+# CHeck if the database file is removed.
+[[ ! -e "/run/udev/data/n$IFINDEX" ]]
+
+udevadm control --log-level=info
+
+exit 0
diff --git a/test/units/TEST-17-UDEV.diskseq.sh b/test/units/TEST-17-UDEV.diskseq.sh
new file mode 100755
index 0000000000..53ee666984
--- /dev/null
+++ b/test/units/TEST-17-UDEV.diskseq.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# shellcheck disable=SC2010
+# shellcheck disable=SC2012
+# shellcheck disable=SC2317
+set -ex
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+# This is a test case for issue #34637.
+
+at_exit() (
+ set +e
+
+ systemctl stop test-diskseq.service || :
+ rm -f /run/systemd/system/test-diskseq.service
+ systemctl daemon-reload
+
+ [[ -d "$TMPDIR" ]] && rm -rf "$TMPDIR"
+
+ udevadm control --log-level=info
+)
+
+trap at_exit EXIT
+
+udevadm control --log-level=debug
+
+TMPDIR="$(mktemp -d)"
+truncate -s 16M "$TMPDIR"/foo.raw
+mkfs.ext4 -L foo "$TMPDIR"/foo.raw
+
+mkdir -p /run/systemd/system/
+cat >/run/systemd/system/test-diskseq.service <<EOF
+[Unit]
+StartLimitIntervalSec=0
+[Service]
+ExecStart=false
+Restart=on-failure
+MountImages=$TMPDIR/foo.raw:/var
+EOF
+systemctl daemon-reload
+
+udevadm settle
+
+# Check if no lock file exists, if the lock directory exists.
+if [[ -d /run/udev/links.lock/ ]]; then
+ [[ "$(ls /run/udev/links.lock/ | wc -l)" == 0 ]]
+fi
+
+# Save the current number of the directories.
+NUM_DISKSEQ=$(ls /run/udev/links/ | grep -c by-diskseq || :)
+
+systemctl start --no-block test-diskseq.service
+
+for _ in {0..100}; do
+ sleep .1
+ n=$(ls /run/udev/links/ | grep -c by-diskseq || :)
+ (( n <= NUM_DISKSEQ + 1 ))
+done
+
+systemctl stop test-diskseq.service || :
+
+udevadm settle
+
+# Check if the lock directory exists, but no lock file exists in it.
+[[ -d /run/udev/links.lock/ ]]
+[[ "$(ls /run/udev/links.lock/ | wc -l)" == 0 ]]
+
+exit 0
diff --git a/test/units/TEST-19-CGROUP.keyed-properties.sh b/test/units/TEST-19-CGROUP.keyed-properties.sh
new file mode 100755
index 0000000000..cadefe26d5
--- /dev/null
+++ b/test/units/TEST-19-CGROUP.keyed-properties.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -ex
+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
+
+if [[ "$(get_cgroup_hierarchy)" != unified ]]; then
+ echo "Skipping $0 as we're not running with the unified cgroup hierarchy"
+ exit 0
+fi
+
+testcase_iodevice_dbus () {
+ # Test that per-device properties are applied in configured order even for different devices (because
+ # they may resolve to same underlying device in the end
+ # Note: if device does not exist cgroup attribute write fails but systemd should still track the
+ # configured properties
+ systemd-run --unit=test0.service \
+ --property="IOAccounting=yes" \
+ sleep inf
+
+ systemctl set-property test0.service \
+ IOReadBandwidthMax="/dev/sda1 1M" \
+ IOReadBandwidthMax="/dev/sda2 2M" \
+ IOReadBandwidthMax="/dev/sda3 4M"
+
+ local output
+ output=$(mktemp)
+ trap 'rm -f "$output"' RETURN
+ systemctl show -P IOReadBandwidthMax test0.service >"$output"
+ diff -u "$output" - <<EOF
+/dev/sda1 1000000
+/dev/sda2 2000000
+/dev/sda3 4000000
+EOF
+
+ systemctl stop test0.service
+}
+
+testcase_iodevice_unitfile () {
+ cat >/run/systemd/system/test1.service <<EOF
+[Service]
+ExecStart=/usr/bin/sleep inf
+IOReadBandwidthMax=/dev/sda1 1M
+IOReadBandwidthMax=/dev/sda2 2M
+IOReadBandwidthMax=/dev/sda3 4M
+EOF
+ systemctl daemon-reload
+
+ local output
+ output=$(mktemp)
+ trap 'rm -f "$output"' RETURN
+ systemctl show -P IOReadBandwidthMax test1.service >"$output"
+ diff -u "$output" - <<EOF
+/dev/sda1 1000000
+/dev/sda2 2000000
+/dev/sda3 4000000
+EOF
+ rm -f /run/systemd/system/test1.service
+}
+
+run_testcases
diff --git a/test/units/TEST-22-TMPFILES.18.sh b/test/units/TEST-22-TMPFILES.18.sh
index 5d24197c81..c81f6bd0ef 100755
--- a/test/units/TEST-22-TMPFILES.18.sh
+++ b/test/units/TEST-22-TMPFILES.18.sh
@@ -9,26 +9,39 @@ set -o pipefail
export SYSTEMD_LOG_LEVEL=debug
c='
-d /tmp/somedir
-f /tmp/somedir/somefile - - - - baz
+d$ /tmp/somedir
+f$ /tmp/somedir/somefile - - - - baz
+f /tmp/someotherfile - - - - qux
'
systemd-tmpfiles --create - <<<"$c"
test -f /tmp/somedir/somefile
grep -q baz /tmp/somedir/somefile
+grep -q qux /tmp/someotherfile
systemd-tmpfiles --purge --dry-run - <<<"$c"
test -f /tmp/somedir/somefile
grep -q baz /tmp/somedir/somefile
+grep -q qux /tmp/someotherfile
systemd-tmpfiles --purge - <<<"$c"
test ! -f /tmp/somedir/somefile
test ! -d /tmp/somedir/
+grep -q qux /tmp/someotherfile
systemd-tmpfiles --create --purge --dry-run - <<<"$c"
test ! -f /tmp/somedir/somefile
test ! -d /tmp/somedir/
+grep -q qux /tmp/someotherfile
systemd-tmpfiles --create --purge - <<<"$c"
test -f /tmp/somedir/somefile
grep -q baz /tmp/somedir/somefile
+grep -q qux /tmp/someotherfile
+
+systemd-tmpfiles --purge - <<<"$c"
+test ! -f /tmp/somedir/somefile
+test ! -d /tmp/somedir/
+grep -q qux /tmp/someotherfile
+
+rm /tmp/someotherfile
diff --git a/test/units/TEST-23-UNIT-FILE-ExtraFileDescriptors-child.sh b/test/units/TEST-23-UNIT-FILE-ExtraFileDescriptors-child.sh
new file mode 100755
index 0000000000..8b182955b5
--- /dev/null
+++ b/test/units/TEST-23-UNIT-FILE-ExtraFileDescriptors-child.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+assert_eq "$LISTEN_FDS" "$1"
+assert_eq "$LISTEN_FDNAMES" "$2"
+
+for ((i = 3; i < 3 + LISTEN_FDS; i++)); do
+ assert_eq "$(cat /proc/self/fd/"$i")" "${!i}" # Dereference $i to get i'th arg
+done
diff --git a/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh b/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh
new file mode 100755
index 0000000000..eaeee635db
--- /dev/null
+++ b/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+at_exit() {
+ set +e
+
+ rm -rf /tmp/test-extra-fd/
+}
+
+trap at_exit EXIT
+
+mkdir /tmp/test-extra-fd
+echo "Hello" > /tmp/test-extra-fd/1.txt
+echo "Extra" > /tmp/test-extra-fd/2.txt
+
+systemd-analyze log-level debug
+
+# Open files and assign FD to variables
+exec {TEST_FD1}</tmp/test-extra-fd/1.txt
+exec {TEST_FD2}</tmp/test-extra-fd/2.txt
+
+TEST_UNIT="test-23-extra-fd.service"
+
+busctl call \
+ org.freedesktop.systemd1 /org/freedesktop/systemd1 \
+ org.freedesktop.systemd1.Manager StartTransientUnit \
+ "ssa(sv)a(sa(sv))" "$TEST_UNIT" replace 4 \
+ ExecStart "a(sasb)" 1 \
+ /usr/lib/systemd/tests/testdata/units/TEST-23-UNIT-FILE-ExtraFileDescriptors-child.sh \
+ 5 /usr/lib/systemd/tests/testdata/units/TEST-23-UNIT-FILE-ExtraFileDescriptors-child.sh 2 "test:other" "Hello" "Extra" \
+ true \
+ RemainAfterExit "b" true \
+ Type "s" oneshot \
+ ExtraFileDescriptors "a(hs)" 2 \
+ "$TEST_FD1" test \
+ "$TEST_FD2" other \
+ 0
+
+cmp -b <(systemctl show -p ExtraFileDescriptorNames "$TEST_UNIT") <<EOF
+ExtraFileDescriptorNames=test other
+EOF
+
+# shellcheck disable=SC2016
+timeout 10s bash -xec 'while [[ "$(systemctl show -P SubState test-23-extra-fd.service)" != "exited" ]]; do sleep .5; done'
+
+assert_eq "$(systemctl show -P Result "$TEST_UNIT")" "success"
+assert_eq "$(systemctl show -P ExecMainStatus "$TEST_UNIT")" "0"
+
+# Verify extra file descriptors stay accessible even after service manager re-executes
+systemctl daemon-reexec
+
+systemctl restart "$TEST_UNIT"
+
+assert_eq "$(systemctl show -P SubState "$TEST_UNIT")" "exited"
+assert_eq "$(systemctl show -P Result "$TEST_UNIT")" "success"
+assert_eq "$(systemctl show -P ExecMainStatus "$TEST_UNIT")" "0"
+
+systemctl stop "$TEST_UNIT"
+
+systemctl log-level info
diff --git a/test/units/TEST-29-PORTABLE.directory.sh b/test/units/TEST-29-PORTABLE.directory.sh
new file mode 100755
index 0000000000..5458111ee2
--- /dev/null
+++ b/test/units/TEST-29-PORTABLE.directory.sh
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+# shellcheck disable=SC2233,SC2235
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+# Arrays cannot be exported, so redefine in each test script
+ARGS=()
+if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then
+ # If we're running under sanitizers, we need to use a less restrictive
+ # profile, otherwise LSan syscall would get blocked by seccomp
+ ARGS+=(--profile=trusted)
+fi
+
+unsquashfs -dest /tmp/minimal_0 /usr/share/minimal_0.raw
+unsquashfs -dest /tmp/minimal_1 /usr/share/minimal_1.raw
+
+portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/minimal_0 minimal-app0
+
+systemctl is-active minimal-app0.service
+systemctl is-active minimal-app0-foo.service
+systemctl is-active minimal-app0-bar.service && exit 1
+
+portablectl "${ARGS[@]}" reattach --now --enable --runtime /tmp/minimal_1 minimal-app0
+
+systemctl is-active minimal-app0.service
+systemctl is-active minimal-app0-bar.service
+systemctl is-active minimal-app0-foo.service && exit 1
+
+portablectl list | grep -q -F "minimal_1"
+busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1'
+
+portablectl detach --now --enable --runtime /tmp/minimal_1 minimal-app0
+
+portablectl list | grep -q -F "No images."
+busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1
+
+mkdir /tmp/rootdir /tmp/app0 /tmp/app1 /tmp/overlay /tmp/os-release-fix /tmp/os-release-fix/etc
+mount /tmp/app0.raw /tmp/app0
+mount /tmp/app1.raw /tmp/app1
+mount /usr/share/minimal_0.raw /tmp/rootdir
+
+# Fix up os-release to drop the valid PORTABLE_SERVICES field (because we are
+# bypassing the sysext logic in portabled here it will otherwise not see the
+# extensions additional valid prefix)
+grep -v "^PORTABLE_PREFIXES=" /tmp/rootdir/etc/os-release >/tmp/os-release-fix/etc/os-release
+
+mount -t overlay overlay -o lowerdir=/tmp/os-release-fix:/tmp/app1:/tmp/rootdir /tmp/overlay
+
+grep . /tmp/overlay/usr/lib/extension-release.d/*
+grep . /tmp/overlay/etc/os-release
+
+portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/overlay app1
+
+systemctl is-active app1.service
+
+portablectl detach --now --runtime overlay app1
+
+# Ensure --force works also when symlinking
+mkdir -p /run/systemd/system.attached/app1.service.d
+cat <<EOF >/run/systemd/system.attached/app1.service
+[Unit]
+Description=App 1
+EOF
+cat <<EOF >/run/systemd/system.attached/app1.service.d/10-profile.conf
+[Unit]
+Description=App 1
+EOF
+cat <<EOF >/run/systemd/system.attached/app1.service.d/20-portable.conf
+[Unit]
+Description=App 1
+EOF
+systemctl daemon-reload
+
+portablectl "${ARGS[@]}" attach --force --copy=symlink --now --runtime /tmp/overlay app1
+
+systemctl is-active app1.service
+
+portablectl detach --now --runtime overlay app1
+
+umount /tmp/overlay
+
+portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
+
+systemctl is-active app0.service
+systemctl is-active app1.service
+
+portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/rootdir/usr/lib/os-release
+portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app0/usr/lib/extension-release.d/extension-release.app0
+portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app1/usr/lib/extension-release.d/extension-release.app2
+portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app1/usr/lib/systemd/system/app1.service
+portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app0/usr/lib/systemd/system/app0.service
+
+grep -q -F "LogExtraFields=PORTABLE=app0" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_ROOT=rootdir" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app1" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1" /run/systemd/system.attached/app0.service.d/20-portable.conf
+
+grep -q -F "LogExtraFields=PORTABLE=app1" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_ROOT=rootdir" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app1" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1" /run/systemd/system.attached/app1.service.d/20-portable.conf
+
+portablectl detach --clean --now --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
+
+# Ensure --clean remove state and other directories belonging to the portable image being detached
+test ! -d /var/lib/app0
+test ! -d /run/app0
+
+# Ensure that mixed mode copies the images and units (client-owned) but symlinks the profile (OS owned)
+portablectl "${ARGS[@]}" attach --copy=mixed --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
+test -d /run/portables/app0
+test -d /run/portables/app1
+test -d /run/portables/rootdir
+test -f /run/systemd/system.attached/app0.service
+test -f /run/systemd/system.attached/app1.service
+test -L /run/systemd/system.attached/app0.service.d/10-profile.conf
+test -L /run/systemd/system.attached/app1.service.d/10-profile.conf
+portablectl detach --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
+
+# Attempt to disable the app unit during detaching. Requires --copy=symlink to reproduce.
+# Provides coverage for https://github.com/systemd/systemd/issues/23481
+portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0
+portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0
+# attach and detach again to check if all drop-in configs are removed even if the main unit files are removed
+portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0
+portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0
+
+# The wrong file should be ignored, given the right one has the xattr set
+trap 'rm -rf /var/cache/wrongext' EXIT
+mkdir -p /var/cache/wrongext/usr/lib/extension-release.d /var/cache/wrongext/usr/lib/systemd/system/
+echo "[Service]" > /var/cache/wrongext/usr/lib/systemd/system/app0.service
+touch /var/cache/wrongext/usr/lib/extension-release.d/extension-release.wrongext_somethingwrong.txt
+cp /tmp/rootdir/usr/lib/os-release /var/cache/wrongext/usr/lib/extension-release.d/extension-release.app0
+setfattr -n user.extension-release.strict -v "false" /var/cache/wrongext/usr/lib/extension-release.d/extension-release.app0
+portablectl "${ARGS[@]}" attach --runtime --extension /var/cache/wrongext /tmp/rootdir app0
+status="$(portablectl is-attached --extension wrongext rootdir)"
+[[ "${status}" == "attached-runtime" ]]
+portablectl detach --runtime --extension /var/cache/wrongext /tmp/rootdir app0
+
+umount /tmp/rootdir
+umount /tmp/app0
+umount /tmp/app1
diff --git a/test/units/TEST-29-PORTABLE.image.sh b/test/units/TEST-29-PORTABLE.image.sh
new file mode 100755
index 0000000000..376d94c283
--- /dev/null
+++ b/test/units/TEST-29-PORTABLE.image.sh
@@ -0,0 +1,240 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+# shellcheck disable=SC2233,SC2235
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+# Arrays cannot be exported, so redefine in each test script
+ARGS=()
+if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then
+ # If we're running under sanitizers, we need to use a less restrictive
+ # profile, otherwise LSan syscall would get blocked by seccomp
+ ARGS+=(--profile=trusted)
+fi
+
+portablectl "${ARGS[@]}" attach --now --runtime /usr/share/minimal_0.raw minimal-app0
+
+portablectl is-attached minimal-app0
+portablectl inspect /usr/share/minimal_0.raw minimal-app0.service
+systemctl is-active minimal-app0.service
+systemctl is-active minimal-app0-foo.service
+systemctl is-active minimal-app0-bar.service && exit 1
+
+portablectl "${ARGS[@]}" reattach --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl is-attached minimal-app0
+portablectl inspect /usr/share/minimal_0.raw minimal-app0.service
+systemctl is-active minimal-app0.service
+systemctl is-active minimal-app0-bar.service
+systemctl is-active minimal-app0-foo.service && exit 1
+
+portablectl list | grep -q -F "minimal_1"
+busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1'
+
+portablectl detach --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl list | grep -q -F "No images."
+busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1
+
+# Ensure we don't regress (again) when using --force
+
+mkdir -p /run/systemd/system.attached/minimal-app0.service.d/
+cat <<EOF >/run/systemd/system.attached/minimal-app0.service
+[Unit]
+Description=Minimal App 0
+EOF
+cat <<EOF >/run/systemd/system.attached/minimal-app0.service.d/10-profile.conf
+[Unit]
+Description=Minimal App 0
+EOF
+cat <<EOF >/run/systemd/system.attached/minimal-app0.service.d/20-portable.conf
+[Unit]
+Description=Minimal App 0
+EOF
+systemctl daemon-reload
+
+portablectl "${ARGS[@]}" attach --force --now --runtime /usr/share/minimal_0.raw minimal-app0
+
+portablectl is-attached --force minimal-app0
+portablectl inspect --force /usr/share/minimal_0.raw minimal-app0.service
+systemctl is-active minimal-app0.service
+systemctl is-active minimal-app0-foo.service
+systemctl is-active minimal-app0-bar.service && exit 1
+
+portablectl "${ARGS[@]}" reattach --force --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl is-attached --force minimal-app0
+portablectl inspect --force /usr/share/minimal_0.raw minimal-app0.service
+systemctl is-active minimal-app0.service
+systemctl is-active minimal-app0-bar.service
+systemctl is-active minimal-app0-foo.service && exit 1
+
+portablectl list | grep -q -F "minimal_1"
+busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1'
+
+portablectl detach --force --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl list | grep -q -F "No images."
+busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1
+
+portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
+
+systemctl is-active app0.service
+status="$(portablectl is-attached --extension app0 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+
+portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0
+
+systemctl is-active app0.service
+status="$(portablectl is-attached --extension app0 minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_1.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+
+portablectl detach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0
+
+# Ensure versioned images are accepted without needing to use --force to override the extension-release
+# matching
+
+cp /tmp/app0.raw /tmp/app0_1.0.raw
+portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app0_1.0.raw /usr/share/minimal_0.raw app0
+
+systemctl is-active app0.service
+status="$(portablectl is-attached --extension app0_1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl detach --now --runtime --extension /tmp/app0_1.0.raw /usr/share/minimal_1.raw app0
+rm -f /tmp/app0_1.0.raw
+
+portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+# Ensure that adding or removing a version to the image doesn't break reattaching
+cp /tmp/app1.raw /tmp/app1_2.raw
+portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1_2.raw /usr/share/minimal_1.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1_2 minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_1.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1 minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl detach --force --no-reload --runtime --extension /tmp/app1.raw /usr/share/minimal_1.raw app1
+portablectl "${ARGS[@]}" attach --force --no-reload --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+systemctl daemon-reload
+systemctl restart app1.service
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl detach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+# Ensure vpick works, including reattaching to a new image
+mkdir -p /tmp/app1.v/
+cp /tmp/app1.raw /tmp/app1.v/app1_1.0.raw
+cp /tmp/app1_2.raw /tmp/app1.v/app1_2.0.raw
+portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1_2.0.raw minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+rm -f /tmp/app1.v/app1_2.0.raw
+portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1_1.0.raw minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl detach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_0.raw app1
+rm -f /tmp/app1.v/app1_1.0.raw
+
+# Ensure that the combination of read-only images, state directory and dynamic user works, and that
+# state is retained. Check after detaching, as on slow systems (eg: sanitizers) it might take a while
+# after the service is attached before the file appears.
+grep -q -F bar "${STATE_DIRECTORY}/app0/foo"
+grep -q -F baz "${STATE_DIRECTORY}/app1/foo"
+
+# Ensure that we can override the check on extension-release.NAME
+cp /tmp/app0.raw /tmp/app10.raw
+portablectl "${ARGS[@]}" attach --force --now --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
+
+systemctl is-active app0.service
+status="$(portablectl is-attached --extension /tmp/app10.raw /usr/share/minimal_0.raw)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl inspect --force --cat --extension /tmp/app10.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /tmp/app10.raw"
+
+# Ensure that we can detach even when an image has been deleted already (stop the unit manually as
+# portablectl won't find it)
+rm -f /tmp/app10.raw
+systemctl stop app0.service
+portablectl detach --force --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
+
+# portablectl also accepts confexts
+portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0
+
+systemctl is-active app0.service
+status="$(portablectl is-attached --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl inspect --force --cat --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /tmp/conf0.raw"
+
+portablectl detach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0
+
+# Ensure that mixed mode copies the images and units (client-owned) but symlinks the profile (OS owned)
+portablectl "${ARGS[@]}" attach --copy=mixed --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
+test -f /run/portables/app0.raw
+test -f /run/portables/minimal_0.raw
+test -f /run/systemd/system.attached/app0.service
+test -L /run/systemd/system.attached/app0.service.d/10-profile.conf
+portablectl detach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
+
+# Ensure that when two portables share the same base image, removing one doesn't remove the other too
+
+portablectl "${ARGS[@]}" attach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
+portablectl "${ARGS[@]}" attach --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+status="$(portablectl is-attached --extension app0 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+status="$(portablectl is-attached --extension app1 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+
+(! portablectl detach --runtime /usr/share/minimal_0.raw app)
+
+status="$(portablectl is-attached --extension app0 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+status="$(portablectl is-attached --extension app1 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+
+# Ensure 'portablectl list' shows the correct status for both images
+portablectl list
+portablectl list | grep -F "minimal_0" | grep -q -F "attached-runtime"
+portablectl list | grep -F "app0" | grep -q -F "attached-runtime"
+portablectl list | grep -F "app1" | grep -q -F "attached-runtime"
+
+portablectl detach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app
+
+status="$(portablectl is-attached --extension app1 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+
+portablectl detach --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app
diff --git a/test/units/TEST-29-PORTABLE.sh b/test/units/TEST-29-PORTABLE.sh
index 501d492c7d..7e0ba7e38a 100755
--- a/test/units/TEST-29-PORTABLE.sh
+++ b/test/units/TEST-29-PORTABLE.sh
@@ -5,6 +5,9 @@
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
@@ -18,9 +21,14 @@ DefaultEnvironment=SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30
ManagerEnvironment=SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30
EOF
+mkdir -p /run/systemd/system/systemd-portabled.service.d/
+cat <<EOF >/run/systemd/system/systemd-portabled.service.d/override.conf
+[Service]
+Environment=SYSTEMD_LOG_LEVEL=debug
+EOF
+
systemctl daemon-reexec
-export SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30
udevadm control --log-level debug
@@ -33,6 +41,11 @@ if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then
# With the trusted profile DynamicUser is disabled, so the storage is not in private/
STATE_DIRECTORY=/var/lib/
fi
+export STATE_DIRECTORY
+export SYSTEMD_LOG_LEVEL=debug
+export SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30
+
+# Quick smoke tests
systemd-dissect --no-pager /usr/share/minimal_0.raw | grep -q '✓ portable service'
systemd-dissect --no-pager /usr/share/minimal_1.raw | grep -q '✓ portable service'
@@ -40,373 +53,6 @@ systemd-dissect --no-pager /tmp/app0.raw | grep -q '✓ sysext for portable serv
systemd-dissect --no-pager /tmp/app1.raw | grep -q '✓ sysext for portable service'
systemd-dissect --no-pager /tmp/conf0.raw | grep -q '✓ confext for portable service'
-export SYSTEMD_LOG_LEVEL=debug
-mkdir -p /run/systemd/system/systemd-portabled.service.d/
-cat <<EOF >/run/systemd/system/systemd-portabled.service.d/override.conf
-[Service]
-Environment=SYSTEMD_LOG_LEVEL=debug
-EOF
-
-portablectl "${ARGS[@]}" attach --now --runtime /usr/share/minimal_0.raw minimal-app0
-
-portablectl is-attached minimal-app0
-portablectl inspect /usr/share/minimal_0.raw minimal-app0.service
-systemctl is-active minimal-app0.service
-systemctl is-active minimal-app0-foo.service
-systemctl is-active minimal-app0-bar.service && exit 1
-
-portablectl "${ARGS[@]}" reattach --now --runtime /usr/share/minimal_1.raw minimal-app0
-
-portablectl is-attached minimal-app0
-portablectl inspect /usr/share/minimal_0.raw minimal-app0.service
-systemctl is-active minimal-app0.service
-systemctl is-active minimal-app0-bar.service
-systemctl is-active minimal-app0-foo.service && exit 1
-
-portablectl list | grep -q -F "minimal_1"
-busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1'
-
-portablectl detach --now --runtime /usr/share/minimal_1.raw minimal-app0
-
-portablectl list | grep -q -F "No images."
-busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1
-
-# Ensure we don't regress (again) when using --force
-
-mkdir -p /run/systemd/system.attached/minimal-app0.service.d/
-cat <<EOF >/run/systemd/system.attached/minimal-app0.service
-[Unit]
-Description=Minimal App 0
-EOF
-cat <<EOF >/run/systemd/system.attached/minimal-app0.service.d/10-profile.conf
-[Unit]
-Description=Minimal App 0
-EOF
-cat <<EOF >/run/systemd/system.attached/minimal-app0.service.d/20-portable.conf
-[Unit]
-Description=Minimal App 0
-EOF
-systemctl daemon-reload
-
-portablectl "${ARGS[@]}" attach --force --now --runtime /usr/share/minimal_0.raw minimal-app0
-
-portablectl is-attached --force minimal-app0
-portablectl inspect --force /usr/share/minimal_0.raw minimal-app0.service
-systemctl is-active minimal-app0.service
-systemctl is-active minimal-app0-foo.service
-systemctl is-active minimal-app0-bar.service && exit 1
-
-portablectl "${ARGS[@]}" reattach --force --now --runtime /usr/share/minimal_1.raw minimal-app0
-
-portablectl is-attached --force minimal-app0
-portablectl inspect --force /usr/share/minimal_0.raw minimal-app0.service
-systemctl is-active minimal-app0.service
-systemctl is-active minimal-app0-bar.service
-systemctl is-active minimal-app0-foo.service && exit 1
-
-portablectl list | grep -q -F "minimal_1"
-busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1'
-
-portablectl detach --force --now --runtime /usr/share/minimal_1.raw minimal-app0
-
-portablectl list | grep -q -F "No images."
-busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1
-
-# portablectl also works with directory paths rather than images
-
-unsquashfs -dest /tmp/minimal_0 /usr/share/minimal_0.raw
-unsquashfs -dest /tmp/minimal_1 /usr/share/minimal_1.raw
-
-portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/minimal_0 minimal-app0
-
-systemctl is-active minimal-app0.service
-systemctl is-active minimal-app0-foo.service
-systemctl is-active minimal-app0-bar.service && exit 1
-
-portablectl "${ARGS[@]}" reattach --now --enable --runtime /tmp/minimal_1 minimal-app0
-
-systemctl is-active minimal-app0.service
-systemctl is-active minimal-app0-bar.service
-systemctl is-active minimal-app0-foo.service && exit 1
-
-portablectl list | grep -q -F "minimal_1"
-busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1'
-
-portablectl detach --now --enable --runtime /tmp/minimal_1 minimal-app0
-
-portablectl list | grep -q -F "No images."
-busctl tree org.freedesktop.portable1 --no-pager | grep -q -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1
-
-portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
-
-systemctl is-active app0.service
-status="$(portablectl is-attached --extension app0 minimal_0)"
-[[ "${status}" == "running-runtime" ]]
-
-grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
-
-portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0
-
-systemctl is-active app0.service
-status="$(portablectl is-attached --extension app0 minimal_1)"
-[[ "${status}" == "running-runtime" ]]
-
-grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_1.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
-
-portablectl detach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0
-
-# Ensure versioned images are accepted without needing to use --force to override the extension-release
-# matching
-
-cp /tmp/app0.raw /tmp/app0_1.0.raw
-portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app0_1.0.raw /usr/share/minimal_0.raw app0
-
-systemctl is-active app0.service
-status="$(portablectl is-attached --extension app0_1 minimal_0)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl detach --now --runtime --extension /tmp/app0_1.0.raw /usr/share/minimal_1.raw app0
-rm -f /tmp/app0_1.0.raw
-
-portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
-
-systemctl is-active app1.service
-status="$(portablectl is-attached --extension app1 minimal_0)"
-[[ "${status}" == "running-runtime" ]]
-
-# Ensure that adding or removing a version to the image doesn't break reattaching
-cp /tmp/app1.raw /tmp/app1_2.raw
-portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1_2.raw /usr/share/minimal_1.raw app1
-
-systemctl is-active app1.service
-status="$(portablectl is-attached --extension app1_2 minimal_1)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_1.raw app1
-
-systemctl is-active app1.service
-status="$(portablectl is-attached --extension app1 minimal_1)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl detach --force --no-reload --runtime --extension /tmp/app1.raw /usr/share/minimal_1.raw app1
-portablectl "${ARGS[@]}" attach --force --no-reload --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
-systemctl daemon-reload
-systemctl restart app1.service
-
-systemctl is-active app1.service
-status="$(portablectl is-attached --extension app1 minimal_0)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl detach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
-
-# Ensure vpick works, including reattaching to a new image
-mkdir -p /tmp/app1.v/
-cp /tmp/app1.raw /tmp/app1.v/app1_1.0.raw
-cp /tmp/app1_2.raw /tmp/app1.v/app1_2.0.raw
-portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
-
-systemctl is-active app1.service
-status="$(portablectl is-attached --extension app1_2.0.raw minimal_1)"
-[[ "${status}" == "running-runtime" ]]
-
-rm -f /tmp/app1.v/app1_2.0.raw
-portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
-
-systemctl is-active app1.service
-status="$(portablectl is-attached --extension app1_1.0.raw minimal_1)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl detach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_0.raw app1
-rm -f /tmp/app1.v/app1_1.0.raw
-
-# Ensure that the combination of read-only images, state directory and dynamic user works, and that
-# state is retained. Check after detaching, as on slow systems (eg: sanitizers) it might take a while
-# after the service is attached before the file appears.
-grep -q -F bar "${STATE_DIRECTORY}/app0/foo"
-grep -q -F baz "${STATE_DIRECTORY}/app1/foo"
-
-# Ensure that we can override the check on extension-release.NAME
-cp /tmp/app0.raw /tmp/app10.raw
-portablectl "${ARGS[@]}" attach --force --now --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
-
-systemctl is-active app0.service
-status="$(portablectl is-attached --extension /tmp/app10.raw /usr/share/minimal_0.raw)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl inspect --force --cat --extension /tmp/app10.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /tmp/app10.raw"
-
-# Ensure that we can detach even when an image has been deleted already (stop the unit manually as
-# portablectl won't find it)
-rm -f /tmp/app10.raw
-systemctl stop app0.service
-portablectl detach --force --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
-
-# portablectl also accepts confexts
-portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0
-
-systemctl is-active app0.service
-status="$(portablectl is-attached --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw)"
-[[ "${status}" == "running-runtime" ]]
-
-portablectl inspect --force --cat --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /tmp/conf0.raw"
-
-portablectl detach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0
-
-# Ensure that mixed mode copies the images and units (client-owned) but symlinks the profile (OS owned)
-portablectl "${ARGS[@]}" attach --copy=mixed --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
-test -f /run/portables/app0.raw
-test -f /run/portables/minimal_0.raw
-test -f /run/systemd/system.attached/app0.service
-test -L /run/systemd/system.attached/app0.service.d/10-profile.conf
-portablectl detach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
-
-# Ensure that when two portables share the same base image, removing one doesn't remove the other too
-
-portablectl "${ARGS[@]}" attach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
-portablectl "${ARGS[@]}" attach --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
-
-status="$(portablectl is-attached --extension app0 minimal_0)"
-[[ "${status}" == "attached-runtime" ]]
-status="$(portablectl is-attached --extension app1 minimal_0)"
-[[ "${status}" == "attached-runtime" ]]
-
-(! portablectl detach --runtime /usr/share/minimal_0.raw app)
-
-status="$(portablectl is-attached --extension app0 minimal_0)"
-[[ "${status}" == "attached-runtime" ]]
-status="$(portablectl is-attached --extension app1 minimal_0)"
-[[ "${status}" == "attached-runtime" ]]
-
-# Ensure 'portablectl list' shows the correct status for both images
-portablectl list
-portablectl list | grep -F "minimal_0" | grep -q -F "attached-runtime"
-portablectl list | grep -F "app0" | grep -q -F "attached-runtime"
-portablectl list | grep -F "app1" | grep -q -F "attached-runtime"
-
-portablectl detach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app
-
-status="$(portablectl is-attached --extension app1 minimal_0)"
-[[ "${status}" == "attached-runtime" ]]
-
-portablectl detach --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app
-
-# portablectl also works with directory paths rather than images
-
-mkdir /tmp/rootdir /tmp/app0 /tmp/app1 /tmp/overlay /tmp/os-release-fix /tmp/os-release-fix/etc
-mount /tmp/app0.raw /tmp/app0
-mount /tmp/app1.raw /tmp/app1
-mount /usr/share/minimal_0.raw /tmp/rootdir
-
-# Fix up os-release to drop the valid PORTABLE_SERVICES field (because we are
-# bypassing the sysext logic in portabled here it will otherwise not see the
-# extensions additional valid prefix)
-grep -v "^PORTABLE_PREFIXES=" /tmp/rootdir/etc/os-release >/tmp/os-release-fix/etc/os-release
-
-mount -t overlay overlay -o lowerdir=/tmp/os-release-fix:/tmp/app1:/tmp/rootdir /tmp/overlay
-
-grep . /tmp/overlay/usr/lib/extension-release.d/*
-grep . /tmp/overlay/etc/os-release
-
-portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/overlay app1
-
-systemctl is-active app1.service
-
-portablectl detach --now --runtime overlay app1
-
-# Ensure --force works also when symlinking
-mkdir -p /run/systemd/system.attached/app1.service.d
-cat <<EOF >/run/systemd/system.attached/app1.service
-[Unit]
-Description=App 1
-EOF
-cat <<EOF >/run/systemd/system.attached/app1.service.d/10-profile.conf
-[Unit]
-Description=App 1
-EOF
-cat <<EOF >/run/systemd/system.attached/app1.service.d/20-portable.conf
-[Unit]
-Description=App 1
-EOF
-systemctl daemon-reload
-
-portablectl "${ARGS[@]}" attach --force --copy=symlink --now --runtime /tmp/overlay app1
-
-systemctl is-active app1.service
-
-portablectl detach --now --runtime overlay app1
-
-umount /tmp/overlay
-
-portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
-
-systemctl is-active app0.service
-systemctl is-active app1.service
-
-portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/rootdir/usr/lib/os-release
-portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app0/usr/lib/extension-release.d/extension-release.app0
-portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app1/usr/lib/extension-release.d/extension-release.app2
-portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app1/usr/lib/systemd/system/app1.service
-portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app0/usr/lib/systemd/system/app0.service
-
-grep -q -F "LogExtraFields=PORTABLE=app0" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_ROOT=rootdir" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app1" /run/systemd/system.attached/app0.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1" /run/systemd/system.attached/app0.service.d/20-portable.conf
-
-grep -q -F "LogExtraFields=PORTABLE=app1" /run/systemd/system.attached/app1.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_ROOT=rootdir" /run/systemd/system.attached/app1.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0" /run/systemd/system.attached/app1.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app1.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app1" /run/systemd/system.attached/app1.service.d/20-portable.conf
-grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1" /run/systemd/system.attached/app1.service.d/20-portable.conf
-
-portablectl detach --clean --now --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
-
-# Ensure --clean remove state and other directories belonging to the portable image being detached
-test ! -d /var/lib/app0
-test ! -d /run/app0
-
-# Ensure that mixed mode copies the images and units (client-owned) but symlinks the profile (OS owned)
-portablectl "${ARGS[@]}" attach --copy=mixed --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
-test -d /run/portables/app0
-test -d /run/portables/app1
-test -d /run/portables/rootdir
-test -f /run/systemd/system.attached/app0.service
-test -f /run/systemd/system.attached/app1.service
-test -L /run/systemd/system.attached/app0.service.d/10-profile.conf
-test -L /run/systemd/system.attached/app1.service.d/10-profile.conf
-portablectl detach --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
-
-# Attempt to disable the app unit during detaching. Requires --copy=symlink to reproduce.
-# Provides coverage for https://github.com/systemd/systemd/issues/23481
-portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0
-portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0
-# attach and detach again to check if all drop-in configs are removed even if the main unit files are removed
-portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0
-portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0
-
-# The wrong file should be ignored, given the right one has the xattr set
-trap 'rm -rf /var/cache/wrongext' EXIT
-mkdir -p /var/cache/wrongext/usr/lib/extension-release.d /var/cache/wrongext/usr/lib/systemd/system/
-echo "[Service]" > /var/cache/wrongext/usr/lib/systemd/system/app0.service
-touch /var/cache/wrongext/usr/lib/extension-release.d/extension-release.wrongext_somethingwrong.txt
-cp /tmp/rootdir/usr/lib/os-release /var/cache/wrongext/usr/lib/extension-release.d/extension-release.app0
-setfattr -n user.extension-release.strict -v "false" /var/cache/wrongext/usr/lib/extension-release.d/extension-release.app0
-portablectl "${ARGS[@]}" attach --runtime --extension /var/cache/wrongext /tmp/rootdir app0
-status="$(portablectl is-attached --extension wrongext rootdir)"
-[[ "${status}" == "attached-runtime" ]]
-portablectl detach --runtime --extension /var/cache/wrongext /tmp/rootdir app0
-
-umount /tmp/rootdir
-umount /tmp/app0
-umount /tmp/app1
-
# Lack of ID field in os-release should be rejected, but it caused a crash in the past instead
mkdir -p /tmp/emptyroot/usr/lib
mkdir -p /tmp/emptyext/usr/lib/extension-release.d
@@ -417,4 +63,8 @@ touch /tmp/emptyext/usr/lib/extension-release.d/extension-release.emptyext
res="$(! portablectl attach --extension /tmp/emptyext /tmp/emptyroot 2> >(grep "Remote peer disconnected"))"
test -z "${res}"
+: "Run subtests"
+
+run_subtests
+
touch /testok
diff --git a/test/units/TEST-50-DISSECT.dissect.sh b/test/units/TEST-50-DISSECT.dissect.sh
index 15f663faf5..6cf1213551 100755
--- a/test/units/TEST-50-DISSECT.dissect.sh
+++ b/test/units/TEST-50-DISSECT.dissect.sh
@@ -427,14 +427,16 @@ systemctl is-active testservice-50e.service
# Check vpick support in ExtensionImages=
VBASE="vtest$RANDOM"
VDIR="/tmp/$VBASE.v"
-mkdir "$VDIR"
+EMPTY_VDIR="/tmp/$VBASE-empty.v"
+NONEXISTENT_VDIR="/tmp/$VBASE-nonexistent.v"
+mkdir "$VDIR" "$EMPTY_VDIR"
ln -s /tmp/app0.raw "$VDIR/${VBASE}_0.raw"
ln -s /tmp/app1.raw "$VDIR/${VBASE}_1.raw"
-systemd-run -P -p ExtensionImages="$VDIR" bash -c '/opt/script1.sh | grep ID'
+systemd-run -P -p ExtensionImages="$VDIR -$EMPTY_VDIR -$NONEXISTENT_VDIR" bash -c '/opt/script1.sh | grep ID'
-rm -rf "$VDIR"
+rm -rf "$VDIR" "$EMPTY_VDIR"
# ExtensionDirectories will set up an overlay
mkdir -p "$IMAGE_DIR/app0" "$IMAGE_DIR/app1" "$IMAGE_DIR/app-nodistro" "$IMAGE_DIR/service-scoped-test"
@@ -502,14 +504,16 @@ systemctl is-active testservice-50f.service
# Check vpick support in ExtensionDirectories=
VBASE="vtest$RANDOM"
VDIR="/tmp/$VBASE.v"
-mkdir "$VDIR"
+EMPTY_VDIR="/tmp/$VBASE-empty.v"
+NONEXISTENT_VDIR="/tmp/$VBASE-nonexistent.v"
+mkdir "$VDIR" "$EMPTY_VDIR"
ln -s "$IMAGE_DIR/app0" "$VDIR/${VBASE}_0"
ln -s "$IMAGE_DIR/app1" "$VDIR/${VBASE}_1"
-systemd-run -P --property ExtensionDirectories="$VDIR" cat /opt/script1.sh | grep -q -F "extension-release.app2"
+systemd-run -P --property ExtensionDirectories="$VDIR -$EMPTY_VDIR -$NONEXISTENT_VDIR" cat /opt/script1.sh | grep -q -F "extension-release.app2"
-rm -rf "$VDIR"
+rm -rf "$VDIR" "$EMPTY_VDIR"
systemd-dissect --umount "$IMAGE_DIR/app0"
systemd-dissect --umount "$IMAGE_DIR/app1"
@@ -527,7 +531,7 @@ systemd-sysext unmerge
rmdir /etc/extensions/app-nodistro
# Similar, but go via varlink
-varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.List '{}'
+varlinkctl call --more /run/systemd/io.systemd.sysext io.systemd.sysext.List '{}'
(! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file )
varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Merge '{}'
grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file
diff --git a/test/units/TEST-54-CREDS.sh b/test/units/TEST-54-CREDS.sh
index 29b789d361..3a4fa654e9 100755
--- a/test/units/TEST-54-CREDS.sh
+++ b/test/units/TEST-54-CREDS.sh
@@ -43,8 +43,8 @@ CRED_DIR="$(mktemp -d)"
ENC_CRED_DIR="$(mktemp -d)"
echo foo >"$CRED_DIR/secure-or-weak"
echo foo >"$CRED_DIR/insecure"
-echo foo | systemd-creds --name="encrypted" encrypt - - | base64 -d >"$ENC_CRED_DIR/encrypted"
-echo foo | systemd-creds encrypt - - | base64 -d >"$ENC_CRED_DIR/encrypted-unnamed"
+echo foo | systemd-creds --name="encrypted" encrypt - "$ENC_CRED_DIR/encrypted"
+echo foo | systemd-creds encrypt - "$ENC_CRED_DIR/encrypted-unnamed"
chmod -R 0400 "$CRED_DIR" "$ENC_CRED_DIR"
chmod -R 0444 "$CRED_DIR/insecure"
mkdir /tmp/empty/
diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh
index bbef52ea51..0af6f6ad28 100755
--- a/test/units/TEST-58-REPART.sh
+++ b/test/units/TEST-58-REPART.sh
@@ -29,6 +29,9 @@ if ! systemd-detect-virt --quiet --container; then
udevadm control --log-level debug
fi
+esp_guid=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
+xbootldr_guid=BC13C2FF-59E6-4262-A352-B275FD6F7172
+
machine="$(uname -m)"
if [ "${machine}" = "x86_64" ]; then
root_guid=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
@@ -968,6 +971,83 @@ EOF
veritysetup dump "${loop}p2" | grep 'Hash block size:' | grep -q '1024'
}
+testcase_verity_hash_size_from_data_size() {
+ local defs imgs loop
+
+ if systemd-detect-virt --quiet --container; then
+ echo "Skipping verity hash size from data size test in container."
+ return
+ fi
+
+ defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")"
+ imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")"
+
+ # shellcheck disable=SC2064
+ trap "rm -rf '$defs' '$imgs'" RETURN
+ chmod 0755 "$defs"
+
+ echo "*** dm-verity-hash-size-from-data-size ***"
+
+ # create minimized data partition with SizeMaxBytes=
+ tee "$defs/verity-data.conf" <<EOF
+[Partition]
+Type=root-${architecture}
+CopyFiles=${defs}
+Verity=data
+VerityMatchKey=root
+Minimize=guess
+SizeMaxBytes=10G
+EOF
+
+ # create hash partition, its size will be derived from SizeMaxBytes= of the data partition
+ tee "$defs/verity-hash.conf" <<EOF
+[Partition]
+Type=root-${architecture}-verity
+Verity=hash
+VerityMatchKey=root
+VerityHashBlockSizeBytes=4096
+VerityDataBlockSizeBytes=4096
+EOF
+
+ systemd-repart --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --seed="$seed" \
+ --dry-run=no \
+ --empty=create \
+ --size=auto \
+ --json=pretty \
+ "$imgs/verity"
+
+ loop="$(losetup --partscan --show --find "$imgs/verity")"
+
+ # Make sure the loopback device gets cleaned up
+ # shellcheck disable=SC2064
+ trap "rm -rf '$defs' '$imgs' ; losetup -d '$loop'" RETURN ERR
+
+ udevadm wait --timeout 60 --settle "${loop:?}p1" "${loop:?}p2"
+
+ output=$(sfdisk -J "$loop")
+
+ # size of the hash partition, as determined by calculate_verity_hash_size()
+ # for 10GiB data partition and hash / data block size of 4096B
+ hash_bytes=84557824
+ hash_sectors_expected=$((hash_bytes / 512))
+
+ hash_sectors_actual=$(jq -r ".partitiontable.partitions | map(select(.name == \"root-${architecture}-verity\")) | .[].size" <<<"$output")
+
+ assert_eq "$hash_sectors_expected" "$hash_sectors_actual"
+
+ data_sectors=$(jq -r ".partitiontable.partitions | map(select(.name == \"root-${architecture}\")) | .[].size" <<<"$output")
+ data_bytes=$((data_sectors * 512))
+ data_verity_blocks=$((data_bytes / 4096))
+
+ # The actual data partition is much smaller than 10GiB, i.e. also smaller than 100MiB
+ assert_rc 0 test $data_bytes -lt $((100 * 1024 * 1024))
+
+ # Check that the verity hash tree is created from the actual on-disk data, not the custom size
+ veritysetup dump "${loop}p2" | grep 'Data blocks:' | grep -q "$data_verity_blocks"
+}
+
testcase_exclude_files() {
local defs imgs root output
@@ -1325,9 +1405,12 @@ testcase_compression() {
# TODO: add btrfs once btrfs-progs v6.11 is available in distributions.
for format in squashfs erofs; do
- if ! command -v "mkfs.$format" && ! command -v mksquashfs >/dev/null; then
- continue
- fi
+ case "$format" in
+ squashfs)
+ command -v mksquashfs >/dev/null || continue ;;
+ *)
+ command -v "mkfs.$format" || continue ;;
+ esac
[[ "$format" == "squashfs" ]] && compression=zstd
[[ "$format" == "erofs" ]] && compression=lz4hc
@@ -1390,6 +1473,121 @@ EOF
[[ "$(sfdisk -d "$imgs/zzz" | grep -F 'uuid=' | awk '{ print $8 }' | sort -u | wc -l)" == "3" ]]
}
+testcase_make_symlinks() {
+ local defs imgs output
+
+ if systemd-detect-virt --quiet --container; then
+ echo "Skipping MakeSymlinks= test in container."
+ return
+ fi
+
+ # For issue #34257
+
+ defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")"
+ imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")"
+ # shellcheck disable=SC2064
+ trap "rm -rf '$defs' '$imgs'" RETURN
+ chmod 0755 "$defs"
+
+ tee "$defs/root.conf" <<EOF
+[Partition]
+Type=root
+MakeDirectories=/dir
+MakeSymlinks=/foo:/bar
+MakeSymlinks=/dir/foo:/bar
+EOF
+
+ systemd-repart --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --empty=create \
+ --size=1G \
+ --dry-run=no \
+ --offline="$OFFLINE" \
+ --json=pretty \
+ "$imgs/zzz"
+
+ systemd-dissect "$imgs/zzz" -M "$imgs/mnt"
+ assert_eq "$(readlink "$imgs/mnt/foo")" "/bar"
+ assert_eq "$(readlink "$imgs/mnt/dir/foo")" "/bar"
+ systemd-dissect -U "$imgs/mnt"
+}
+
+testcase_fallback_partitions() {
+ local workdir image defs
+
+ workdir="$(mktemp --directory "/tmp/test-repart.fallback.XXXXXXXXXX")"
+ # shellcheck disable=SC2064
+ trap "rm -rf '${workdir:?}'" RETURN
+
+ image="$workdir/image.img"
+ defs="$workdir/defs"
+ mkdir "$defs"
+
+ tee "$defs/10-esp.conf" <<EOF
+[Partition]
+Type=esp
+Format=vfat
+SizeMinBytes=10M
+EOF
+
+ tee "$defs/20-xbootldr.conf" <<EOF
+[Partition]
+Type=xbootldr
+Format=vfat
+SizeMinBytes=100M
+SupplementFor=10-esp
+EOF
+
+ # Blank disk => big ESP should be created
+
+ systemd-repart --empty=create --size=auto --dry-run=no --definitions="$defs" "$image"
+
+ output=$(sfdisk -d "$image")
+ assert_in "${image}1 : start= 2048, size= 204800, type=${esp_guid}" "$output"
+ assert_not_in "${image}2" "$output"
+
+ # Disk with small ESP => ESP grows
+
+ sfdisk "$image" <<EOF
+label: gpt
+size=10M, type=${esp_guid}
+EOF
+
+ systemd-repart --dry-run=no --definitions="$defs" "$image"
+
+ output=$(sfdisk -d "$image")
+ assert_in "${image}1 : start= 2048, size= 204800, type=${esp_guid}" "$output"
+ assert_not_in "${image}2" "$output"
+
+ # Disk with small ESP that can't grow => XBOOTLDR created
+
+ truncate -s 150M "$image"
+ sfdisk "$image" <<EOF
+label: gpt
+size=10M, type=${esp_guid},
+size=10M, type=${root_guid},
+EOF
+
+ systemd-repart --dry-run=no --definitions="$defs" "$image"
+
+ output=$(sfdisk -d "$image")
+ assert_in "${image}1 : start= 2048, size= 20480, type=${esp_guid}" "$output"
+ assert_in "${image}3 : start= 43008, size= 264152, type=${xbootldr_guid}" "$output"
+
+ # Disk with existing XBOOTLDR partition => XBOOTLDR grows, small ESP created
+
+ sfdisk "$image" <<EOF
+label: gpt
+size=10M, type=${xbootldr_guid},
+EOF
+
+ systemd-repart --dry-run=no --definitions="$defs" "$image"
+
+ output=$(sfdisk -d "$image")
+ assert_in "${image}1 : start= 2048, size= 204800, type=${xbootldr_guid}" "$output"
+ assert_in "${image}2 : start= 206848, size= 100312, type=${esp_guid}" "$output"
+}
+
OFFLINE="yes"
run_testcases
diff --git a/test/units/TEST-65-ANALYZE.sh b/test/units/TEST-65-ANALYZE.sh
index 9fc2de1a21..c00559f6fb 100755
--- a/test/units/TEST-65-ANALYZE.sh
+++ b/test/units/TEST-65-ANALYZE.sh
@@ -172,6 +172,13 @@ systemd-analyze calendar --base-time=yesterday --iterations=5 '*-* *:*:*'
systemd-analyze timestamp now
systemd-analyze timestamp -- -1
systemd-analyze timestamp yesterday now tomorrow
+systemd-analyze timestamp 'Fri 2012-11-23 23:02:15'
+systemd-analyze timestamp 'Fri 2012-11-23 23:02:15 UTC'
+systemd-analyze timestamp 'Fri 2012-11-23 23:02:15 CET'
+for i in $(timedatectl list-timezones); do
+ [[ -e "/usr/share/zoneinfo/$i" ]] || continue
+ systemd-analyze timestamp "Fri 2012-11-23 23:02:15 $i"
+done
(! systemd-analyze timestamp yesterday never tomorrow)
(! systemd-analyze timestamp 1)
(! systemd-analyze timestamp '*-2-29 0:0:0')
@@ -381,6 +388,29 @@ systemd-analyze verify /tmp/multi-exec-start.service
echo 'ExecStart=command-should-not-exist' >>/tmp/multi-exec-start.service
(! systemd-analyze verify /tmp/multi-exec-start.service)
+# Prevent regression from #20233 where systemd-analyze will return nonzero exit codes on warnings
+
+# Unit file with warning "Unknown key name 'foo' in section 'Unit', ignoring"
+cat <<EOF >/tmp/testwarnings.service
+[Unit]
+Foo=Bar
+
+[Service]
+ExecStart=echo hello
+EOF
+
+# yes/no/one should all return nonzero exit status for warnings in unit file
+(! systemd-analyze verify --recursive-errors=yes /tmp/testwarnings.service)
+
+(! systemd-analyze verify --recursive-errors=no /tmp/testwarnings.service)
+
+(! systemd-analyze verify --recursive-errors=one /tmp/testwarnings.service)
+
+# zero exit status since no errors and only warnings
+systemd-analyze verify /tmp/testwarnings.service
+
+rm /tmp/testwarnings.service
+
# Added an additional "INVALID_ID" id to the .json to verify that nothing breaks when input is malformed
# The PrivateNetwork id description and weight was changed to verify that 'security' is actually reading in
# values from the .json file when required. The default weight for "PrivateNetwork" is 2500, and the new weight
@@ -965,6 +995,13 @@ systemd-analyze condition --instance=tmp --unit=systemd-growfs@.service
systemd-analyze verify --instance=tmp --man=no systemd-growfs@.service
systemd-analyze security --instance=tmp systemd-growfs@.service
+systemd-analyze has-tpm2 ||:
+if systemd-analyze has-tpm2 -q ; then
+ echo "have tpm2"
+else
+ echo "have no tpm2"
+fi
+
systemd-analyze log-level info
touch /testok
diff --git a/test/units/TEST-67-INTEGRITY.sh b/test/units/TEST-67-INTEGRITY.sh
index a42fd66aa5..eb3082d132 100755
--- a/test/units/TEST-67-INTEGRITY.sh
+++ b/test/units/TEST-67-INTEGRITY.sh
@@ -52,7 +52,7 @@ fi
# Do one iteration with a separate data device, to test those branches
separate_data=1
-for algorithm in crc32c crc32 sha1 sha256
+for algorithm in crc32c crc32 xxhash64 sha1 sha256
do
if [ "${separate_data}" -eq 1 ]; then
data_option="--data-device=${image_dir}/data"
diff --git a/test/units/TEST-74-AUX-UTILS.busctl.sh b/test/units/TEST-74-AUX-UTILS.busctl.sh
index 3ef94e5cb9..4949f4bac7 100755
--- a/test/units/TEST-74-AUX-UTILS.busctl.sh
+++ b/test/units/TEST-74-AUX-UTILS.busctl.sh
@@ -46,6 +46,10 @@ busctl call -j \
busctl call --verbose --timeout=60 --expect-reply=yes \
org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \
ListUnitsByPatterns asas 1 "active" 2 "systemd-*.socket" "*.mount"
+# show information passed fd
+busctl call --json=pretty \
+ org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \
+ DumpByFileDescriptor | jq
busctl emit /org/freedesktop/login1 org.freedesktop.login1.Manager \
PrepareForSleep b false
@@ -53,6 +57,10 @@ busctl emit --auto-start=no --destination=systemd-logind.service \
/org/freedesktop/login1 org.freedesktop.login1.Manager \
PrepareForShutdown b false
+systemd-run --quiet --service-type=notify --unit=test-busctl-wait --pty \
+ -p ExecStartPost="busctl emit /test org.freedesktop.fake1 TestSignal s success" \
+ busctl --timeout=3 wait /test org.freedesktop.fake1 TestSignal | grep -qF 's "success"'
+
busctl get-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \
Version
busctl get-property --verbose \
diff --git a/test/units/TEST-74-AUX-UTILS.defer_reactivation.sh b/test/units/TEST-74-AUX-UTILS.defer_reactivation.sh
new file mode 100755
index 0000000000..d1b113d1ab
--- /dev/null
+++ b/test/units/TEST-74-AUX-UTILS.defer_reactivation.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+systemctl start realtime-test.timer
+
+sleep 35
+mindelta=10
+
+last=
+while read -r time; do
+ if [ -n "$last" ]; then
+ delta=$((time - last))
+ if [ "$delta" -lt $mindelta ]; then
+ echo "Timer fired too early: $delta < $mindelta" >/failed
+ break
+ fi
+ fi
+ last=$time
+done </tmp/realtime-test.log
+
+test ! -s /failed
diff --git a/test/units/TEST-74-AUX-UTILS.networkctl.sh b/test/units/TEST-74-AUX-UTILS.networkctl.sh
index 0576d6c055..3d402a7182 100755
--- a/test/units/TEST-74-AUX-UTILS.networkctl.sh
+++ b/test/units/TEST-74-AUX-UTILS.networkctl.sh
@@ -21,6 +21,9 @@ at_exit() {
trap at_exit EXIT
+systemctl unmask systemd-networkd.service
+systemctl start systemd-networkd.service
+
export NETWORK_NAME="10-networkctl-test-$RANDOM.network"
export NETDEV_NAME="10-networkctl-test-$RANDOM.netdev"
export LINK_NAME="10-networkctl-test-$RANDOM.link"
@@ -75,15 +78,6 @@ cmp "+4" "/etc/systemd/network/${NETWORK_NAME}.d/test.conf"
networkctl cat "$NETWORK_NAME" | grep '^# ' |
cmp - <(printf '%s\n' "# /etc/systemd/network/$NETWORK_NAME" "# /etc/systemd/network/${NETWORK_NAME}.d/test.conf")
-networkctl edit --stdin --runtime "$NETDEV_NAME" <<EOF
-[NetDev]
-Name=test2
-Kind=dummy
-EOF
-
-networkctl cat "$NETDEV_NAME" | grep -v '^# ' |
- cmp - <(printf '%s\n' "[NetDev]" "Name=test2" "Kind=dummy")
-
cat >"/usr/lib/systemd/network/$LINK_NAME" <<EOF
[Match]
OriginalName=test2
@@ -95,13 +89,23 @@ EOF
SYSTEMD_LOG_LEVEL=debug EDITOR='true' script -ec 'networkctl edit "$LINK_NAME"' /dev/null
cmp "/usr/lib/systemd/network/$LINK_NAME" "/etc/systemd/network/$LINK_NAME"
-# Test links
-systemctl unmask systemd-networkd
-systemctl stop systemd-networkd
+# The interface test2 does not exist, hence the below do not work.
(! networkctl cat @test2)
(! networkctl cat @test2:netdev)
+(! networkctl cat @test2:link)
+(! networkctl cat @test2:network)
+
+# create .netdev file at last, otherwise, the .link file will not be applied to the interface.
+networkctl edit --stdin --runtime "$NETDEV_NAME" <<EOF
+[NetDev]
+Name=test2
+Kind=dummy
+EOF
+
+networkctl cat "$NETDEV_NAME" | grep -v '^# ' |
+ cmp - <(printf '%s\n' "[NetDev]" "Name=test2" "Kind=dummy")
-systemctl start systemd-networkd
+# wait for the interface being created and configured.
SYSTEMD_LOG_LEVEL=debug /usr/lib/systemd/systemd-networkd-wait-online -i test2:carrier --timeout 20
networkctl cat @test2:network | cmp - <(networkctl cat "$NETWORK_NAME")
diff --git a/test/units/TEST-74-AUX-UTILS.run.sh b/test/units/TEST-74-AUX-UTILS.run.sh
index 9e0a429e41..5b46e11409 100755
--- a/test/units/TEST-74-AUX-UTILS.run.sh
+++ b/test/units/TEST-74-AUX-UTILS.run.sh
@@ -246,4 +246,16 @@ if [[ -e /usr/lib/pam.d/systemd-run0 ]] || [[ -e /etc/pam.d/systemd-run0 ]]; the
# Let's chain a couple of run0 calls together, for fun
readarray -t cmdline < <(printf "%.0srun0\n" {0..31})
assert_eq "$("${cmdline[@]}" bash -c 'echo $SUDO_USER')" "$USER"
+
+ # Tests for working directory, especially for specifying "/" (see PR #34771).
+ cd /
+ assert_eq "$(run0 pwd)" "/"
+ assert_eq "$(run0 -D /tmp pwd)" "/tmp"
+ assert_eq "$(run0 --user=testuser pwd)" "/home/testuser"
+ assert_eq "$(run0 -D /tmp --user=testuser pwd)" "/tmp"
+ cd /tmp
+ assert_eq "$(run0 pwd)" "/tmp"
+ assert_eq "$(run0 -D / pwd)" "/"
+ assert_eq "$(run0 --user=testuser pwd)" "/home/testuser"
+ assert_eq "$(run0 -D / --user=testuser pwd)" "/"
fi
diff --git a/test/units/TEST-86-MULTI-PROFILE-UKI.sh b/test/units/TEST-86-MULTI-PROFILE-UKI.sh
new file mode 100755
index 0000000000..042cc59419
--- /dev/null
+++ b/test/units/TEST-86-MULTI-PROFILE-UKI.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+export SYSTEMD_LOG_LEVEL=debug
+
+bootctl
+
+CURRENT_UKI=$(bootctl --print-stub-path)
+
+echo "CURRENT UKI ($CURRENT_UKI):"
+ukify inspect "$CURRENT_UKI"
+if test -f /run/systemd/stub/profile; then
+ echo "CURRENT PROFILE:"
+ cat /run/systemd/stub/profile
+fi
+echo "CURRENT MEASUREMENT:"
+/usr/lib/systemd/systemd-measure --current
+if test -f /run/systemd/tpm2-pcr-signature.json; then
+ echo "CURRENT SIGNATURE:"
+ jq </run/systemd/tpm2-pcr-signature.json
+fi
+
+echo "CURRENT EVENT LOG + PCRS:"
+/usr/lib/systemd/systemd-pcrlock
+
+if test ! -f /run/systemd/stub/profile; then
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /root/pcrsign.private.pem
+ openssl rsa -pubout -in /root/pcrsign.private.pem -out /root/pcrsign.public.pem
+
+ ukify build --extend="$CURRENT_UKI" --output=/tmp/extended0.efi --profile='ID=profile0
+TITLE="Profile Zero"' --measure-base="$CURRENT_UKI" --pcr-private-key=/root/pcrsign.private.pem --pcr-public-key=/root/pcrsign.public.pem --pcr-banks=sha256,sha384,sha512
+
+ ukify build --extend=/tmp/extended0.efi --output=/tmp/extended1.efi --profile='ID=profile1
+TITLE="Profile One"' --measure-base=/tmp/extended0.efi --cmdline="testprofile1=1 $(cat /proc/cmdline)" --pcr-private-key=/root/pcrsign.private.pem --pcr-public-key=/root/pcrsign.public.pem --pcr-banks=sha256,sha384,sha512
+
+ ukify build --extend=/tmp/extended1.efi --output=/tmp/extended2.efi --profile='ID=profile2
+TITLE="Profile Two"' --measure-base=/tmp/extended1.efi --cmdline="testprofile2=1 $(cat /proc/cmdline)" --pcr-private-key=/root/pcrsign.private.pem --pcr-public-key=/root/pcrsign.public.pem --pcr-banks=sha256,sha384,sha512
+
+ echo "EXTENDED UKI:"
+ ukify inspect /tmp/extended2.efi
+ rm /tmp/extended0.efi /tmp/extended1.efi
+ mv /tmp/extended2.efi "$CURRENT_UKI"
+
+ # Prepare a disk image, locked to the PCR measurements of the UKI we just generated
+ truncate -s 32M /root/encrypted.raw
+ echo -n "geheim" >/root/encrypted.secret
+ cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom /root/encrypted.raw --key-file=/root/encrypted.secret
+ systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs= --tpm2-public-key=/root/pcrsign.public.pem --unlock-key-file=/root/encrypted.secret /root/encrypted.raw
+ rm -f /root/encrypted.secret
+
+ reboot
+ exit 0
+else
+ # shellcheck source=/dev/null
+ . /run/systemd/stub/profile
+
+ # Validate that with the current profile we can fulfill the PCR 11 policy
+ systemd-cryptsetup attach multiprof /root/encrypted.raw - tpm2-device=auto,headless=1
+ systemd-cryptsetup detach multiprof
+
+ if [ "$ID" = "profile0" ]; then
+ grep -v testprofile /proc/cmdline
+ echo "default $(basename "$CURRENT_UKI")@profile1" >"$(bootctl -p)/loader/loader.conf"
+ reboot
+ exit 0
+ elif [ "$ID" = "profile1" ]; then
+ grep testprofile1=1 /proc/cmdline
+ echo "default $(basename "$CURRENT_UKI")@profile2" >"$(bootctl -p)/loader/loader.conf"
+ reboot
+ exit 0
+ elif [ "$ID" = "profile2" ]; then
+ grep testprofile2=1 /proc/cmdline
+ rm /root/encrypted.raw
+ else
+ exit 1
+ fi
+fi
+
+touch /testok