diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /test/units | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/units')
312 files changed, 26678 insertions, 0 deletions
diff --git a/test/units/a-conj.service b/test/units/a-conj.service new file mode 100644 index 0000000..3a7c9e1 --- /dev/null +++ b/test/units/a-conj.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=A conjugate +Requires=a.service +After=a.service +Before=a.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/a.service b/test/units/a.service new file mode 100644 index 0000000..ec5d059 --- /dev/null +++ b/test/units/a.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=A +Requires=b.service +Before=b.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/autorelabel.service b/test/units/autorelabel.service new file mode 100644 index 0000000..7e5f9a2 --- /dev/null +++ b/test/units/autorelabel.service @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Relabel all filesystems +DefaultDependencies=no +Requires=local-fs.target +Conflicts=shutdown.target +After=local-fs.target +Before=sysinit.target shutdown.target +ConditionSecurity=selinux +ConditionPathExists=|/.autorelabel + +[Service] +ExecStart=sh -xec 'echo 0 >/sys/fs/selinux/enforce; fixfiles -f -F relabel; rm /.autorelabel; systemctl --force reboot' +Type=oneshot +TimeoutSec=infinity +RemainAfterExit=yes + +[Install] +WantedBy=basic.target diff --git a/test/units/b.service b/test/units/b.service new file mode 100644 index 0000000..4503cf3 --- /dev/null +++ b/test/units/b.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=B +Wants=f.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/basic.target b/test/units/basic.target new file mode 100644 index 0000000..d8cdd5a --- /dev/null +++ b/test/units/basic.target @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Basic System +Documentation=man:systemd.special(7) +Requires=sysinit.target +Wants=sockets.target timers.target paths.target slices.target +After=sysinit.target sockets.target paths.target slices.target tmp.mount + +# We support /var, /tmp, /var/tmp, being on NFS, but we don't pull in +# remote-fs.target by default, hence pull them in explicitly here. Note that we +# require /var and /var/tmp, but only add a Wants= type dependency on /tmp, as +# we support that unit being masked, and this should not be considered an error. +RequiresMountsFor=/var /var/tmp +Wants=tmp.mount diff --git a/test/units/c.service b/test/units/c.service new file mode 100644 index 0000000..a1ce28c --- /dev/null +++ b/test/units/c.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=C +Requires=a.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/d.service b/test/units/d.service new file mode 100644 index 0000000..8202325 --- /dev/null +++ b/test/units/d.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=D:Cyclic +After=b.service +Before=a.service +Requires=a.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/daughter.service b/test/units/daughter.service new file mode 100644 index 0000000..385fbed --- /dev/null +++ b/test/units/daughter.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Daughter Service + +[Service] +Slice=parent.slice +Type=oneshot +ExecStart=/bin/true +CPUAccounting=true diff --git a/test/units/delegated_cgroup_filtering_payload.sh b/test/units/delegated_cgroup_filtering_payload.sh new file mode 100755 index 0000000..50d01a5 --- /dev/null +++ b/test/units/delegated_cgroup_filtering_payload.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# SPDX-License-Identifier: LGPL-2.1-or-later + +mkdir /sys/fs/cgroup/system.slice/delegated-cgroup-filtering.service/the_child +/bin/sh /usr/lib/systemd/tests/testdata/units/delegated_cgroup_filtering_payload_child.sh & + +while true +do + echo "parent_process: hello, world!" + echo "parent_process: hello, people!" + sleep .15 +done diff --git a/test/units/delegated_cgroup_filtering_payload_child.sh b/test/units/delegated_cgroup_filtering_payload_child.sh new file mode 100755 index 0000000..b5635b5 --- /dev/null +++ b/test/units/delegated_cgroup_filtering_payload_child.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# SPDX-License-Identifier: LGPL-2.1-or-later + +echo $$ >/sys/fs/cgroup/system.slice/delegated-cgroup-filtering.service/the_child/cgroup.procs + +while true +do + echo "child_process: hello, world!" + echo "child_process: hello, people!" + sleep .15 +done diff --git a/test/units/dml-discard-empty.service b/test/units/dml-discard-empty.service new file mode 100644 index 0000000..720c1da --- /dev/null +++ b/test/units/dml-discard-empty.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML discard empty service + +[Service] +Slice=dml-discard.slice +Type=oneshot +ExecStart=/bin/true diff --git a/test/units/dml-discard-set-ml.service b/test/units/dml-discard-set-ml.service new file mode 100644 index 0000000..93246ac --- /dev/null +++ b/test/units/dml-discard-set-ml.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML discard set ml service + +[Service] +Slice=dml-discard.slice +Type=oneshot +ExecStart=/bin/true +MemoryLow=15 diff --git a/test/units/dml-discard.slice b/test/units/dml-discard.slice new file mode 100644 index 0000000..dc8a397 --- /dev/null +++ b/test/units/dml-discard.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML discard slice + +[Slice] +DefaultMemoryLow= diff --git a/test/units/dml-override-empty.service b/test/units/dml-override-empty.service new file mode 100644 index 0000000..ac96de0 --- /dev/null +++ b/test/units/dml-override-empty.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML override empty service + +[Service] +Slice=dml-override.slice +Type=oneshot +ExecStart=/bin/true diff --git a/test/units/dml-override.slice b/test/units/dml-override.slice new file mode 100644 index 0000000..ac664d1 --- /dev/null +++ b/test/units/dml-override.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML override slice + +[Slice] +DefaultMemoryLow=10 diff --git a/test/units/dml-passthrough-empty.service b/test/units/dml-passthrough-empty.service new file mode 100644 index 0000000..1e1ba34 --- /dev/null +++ b/test/units/dml-passthrough-empty.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML passthrough empty service + +[Service] +Slice=dml-passthrough.slice +Type=oneshot +ExecStart=/bin/true diff --git a/test/units/dml-passthrough-set-dml.service b/test/units/dml-passthrough-set-dml.service new file mode 100644 index 0000000..9a15311 --- /dev/null +++ b/test/units/dml-passthrough-set-dml.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML passthrough set DML service + +[Service] +Slice=dml-passthrough.slice +Type=oneshot +ExecStart=/bin/true +DefaultMemoryLow=15 diff --git a/test/units/dml-passthrough-set-ml.service b/test/units/dml-passthrough-set-ml.service new file mode 100644 index 0000000..65083bc --- /dev/null +++ b/test/units/dml-passthrough-set-ml.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML passthrough set ML service + +[Service] +Slice=dml-passthrough.slice +Type=oneshot +ExecStart=/bin/true +MemoryLow=0 diff --git a/test/units/dml-passthrough.slice b/test/units/dml-passthrough.slice new file mode 100644 index 0000000..1c8769d --- /dev/null +++ b/test/units/dml-passthrough.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML passthrough slice + +[Slice] +MemoryLow=100 diff --git a/test/units/dml.slice b/test/units/dml.slice new file mode 100644 index 0000000..8e00e7f --- /dev/null +++ b/test/units/dml.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=DML slice + +[Slice] +DefaultMemoryLow=50 diff --git a/test/units/e.service b/test/units/e.service new file mode 100644 index 0000000..5bbcde2 --- /dev/null +++ b/test/units/e.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=E:Cyclic +After=b.service +Before=a.service +Wants=a.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/end.service b/test/units/end.service new file mode 100644 index 0000000..50a68b9 --- /dev/null +++ b/test/units/end.service @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=End the test +After=testsuite.target +OnFailure=poweroff.target +OnFailureJobMode=replace-irreversibly + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/end.sh +TimeoutStartSec=5m diff --git a/test/units/end.sh b/test/units/end.sh new file mode 100755 index 0000000..230b716 --- /dev/null +++ b/test/units/end.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +set -eux +set -o pipefail + +(! journalctl -q -o short-monotonic --grep "didn't pass validation" >>/failed) + +# Here, the redundant '[.]' at the end is for making not the logged self command hit the grep. +(! journalctl -q -o short-monotonic --grep 'Attempted to close sd-bus after fork whose connection is opened before the fork, this should not happen[.]' >>/failed) + +systemctl poweroff --no-block +exit 0 diff --git a/test/units/f.service b/test/units/f.service new file mode 100644 index 0000000..ca20053 --- /dev/null +++ b/test/units/f.service @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=F + +[Service] +ExecStart=/bin/true diff --git a/test/units/g.service b/test/units/g.service new file mode 100644 index 0000000..5fd794d --- /dev/null +++ b/test/units/g.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=G +Conflicts=e.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/generator-utils.sh b/test/units/generator-utils.sh new file mode 100755 index 0000000..fb62747 --- /dev/null +++ b/test/units/generator-utils.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +link_endswith() { + [[ -h "${1:?}" && "$(readlink "${1:?}")" =~ ${2:?}$ ]] +} + +link_eq() { + [[ -h "${1:?}" && "$(readlink "${1:?}")" == "${2:?}" ]] +} + +# Get the value from a 'key=value' assignment +opt_get_arg() { + local arg + + IFS="=" read -r _ arg <<< "${1:?}" + test -n "$arg" + echo "$arg" +} + +in_initrd() { + [[ "${SYSTEMD_IN_INITRD:-0}" -ne 0 ]] +} + +# Check if we're parsing host's fstab in initrd +in_initrd_host() { + in_initrd && [[ "${SYSTEMD_SYSROOT_FSTAB:-/dev/null}" != /dev/null ]] +} + +in_container() { + systemd-detect-virt -qc +} + +opt_filter() ( + set +x + local opt split_options filtered_options + + IFS="," read -ra split_options <<< "${1:?}" + for opt in "${split_options[@]}"; do + if [[ "$opt" =~ ${2:?} ]]; then + continue + fi + + filtered_options+=("$opt") + done + + IFS=","; printf "%s" "${filtered_options[*]}" +) + +# Run the given generator $1 with target directory $2 - clean the target +# directory beforehand +run_and_list() { + local generator="${1:?}" + local out_dir="${2:?}" + local environ + + # If $PID1_ENVIRON is set temporarily overmount /proc/1/environ with + # a temporary file that contains contents of $PID1_ENVIRON. This is + # necessary in cases where the generator reads the environment through + # getenv_for_pid(1, ...) or similar like getty-generator does. + # + # Note: $PID1_ENVIRON should be a NUL separated list of env assignments + if [[ -n "${PID1_ENVIRON:-}" ]]; then + environ="$(mktemp)" + echo -ne "${PID1_ENVIRON}\0" >"${environ:?}" + mount -v --bind "$environ" /proc/1/environ + fi + + rm -fr "${out_dir:?}"/* + mkdir -p "$out_dir"/{normal,early,late} + SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-debug}" "$generator" "$out_dir/normal" "$out_dir/early" "$out_dir/late" + ls -lR "$out_dir" + + if [[ -n "${environ:-}" ]]; then + umount /proc/1/environ + rm -f "$environ" + fi +} diff --git a/test/units/grandchild.service b/test/units/grandchild.service new file mode 100644 index 0000000..4fe77b4 --- /dev/null +++ b/test/units/grandchild.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Grandchild Service + +[Service] +Slice=parent-deep.slice +Type=oneshot +ExecStart=/bin/true diff --git a/test/units/h.service b/test/units/h.service new file mode 100644 index 0000000..5361d42 --- /dev/null +++ b/test/units/h.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=H +Wants=g.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/i.service b/test/units/i.service new file mode 100644 index 0000000..2b5e821 --- /dev/null +++ b/test/units/i.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=I +Conflicts=a.service d.service +Wants=b.service +After=b.service + +[Service] +ExecStart=/bin/true diff --git a/test/units/loopy.service b/test/units/loopy.service new file mode 100644 index 0000000..7fc0e42 --- /dev/null +++ b/test/units/loopy.service @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Service] +ExecStart=/bin/true diff --git a/test/units/loopy.service.d/compat.conf b/test/units/loopy.service.d/compat.conf new file mode 100644 index 0000000..53d213c --- /dev/null +++ b/test/units/loopy.service.d/compat.conf @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +BindsTo=loopy2.service + +[Install] +Also=loopy2.service diff --git a/test/units/loopy2.service b/test/units/loopy2.service new file mode 100644 index 0000000..7fc0e42 --- /dev/null +++ b/test/units/loopy2.service @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Service] +ExecStart=/bin/true diff --git a/test/units/loopy3.service b/test/units/loopy3.service new file mode 100644 index 0000000..b2af20a --- /dev/null +++ b/test/units/loopy3.service @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Service] +ExecStart=/bin/true + +[Unit] +Conflicts=loopy4.service diff --git a/test/units/loopy4.service b/test/units/loopy4.service new file mode 100644 index 0000000..b2af20a --- /dev/null +++ b/test/units/loopy4.service @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Service] +ExecStart=/bin/true + +[Unit] +Conflicts=loopy4.service diff --git a/test/units/nomem.slice b/test/units/nomem.slice new file mode 100644 index 0000000..f4837da --- /dev/null +++ b/test/units/nomem.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Nomem Parent Slice + +[Slice] +DisableControllers=memory diff --git a/test/units/nomemleaf.service b/test/units/nomemleaf.service new file mode 100644 index 0000000..14ce5ad --- /dev/null +++ b/test/units/nomemleaf.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Nomem Leaf Service + +[Service] +Slice=nomem.slice +Type=oneshot +ExecStart=/bin/true +IOWeight=200 +MemoryAccounting=true diff --git a/test/units/parent-deep.slice b/test/units/parent-deep.slice new file mode 100644 index 0000000..983ed65 --- /dev/null +++ b/test/units/parent-deep.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Deeper Parent Slice + +[Slice] +MemoryLimit=3G diff --git a/test/units/parent.slice b/test/units/parent.slice new file mode 100644 index 0000000..f49530b --- /dev/null +++ b/test/units/parent.slice @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Parent Slice + +[Slice] +IOWeight=200 diff --git a/test/units/sched_idle_bad.service b/test/units/sched_idle_bad.service new file mode 100644 index 0000000..be8f1c2 --- /dev/null +++ b/test/units/sched_idle_bad.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Bad sched priority for Idle + +[Service] +ExecStart=/bin/true +CPUSchedulingPriority=1 diff --git a/test/units/sched_idle_ok.service b/test/units/sched_idle_ok.service new file mode 100644 index 0000000..5a1d809 --- /dev/null +++ b/test/units/sched_idle_ok.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Sched idle with prio 0 + +[Service] +ExecStart=/bin/true +CPUSchedulingPriority=0 diff --git a/test/units/sched_rr_bad.service b/test/units/sched_rr_bad.service new file mode 100644 index 0000000..b51b868 --- /dev/null +++ b/test/units/sched_rr_bad.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Bad sched priority for RR + +[Service] +ExecStart=/bin/true +CPUSchedulingPriority=-1 +CPUSchedulingPriority=100 +CPUSchedulingPolicy=rr diff --git a/test/units/sched_rr_change.service b/test/units/sched_rr_change.service new file mode 100644 index 0000000..6ae1feb --- /dev/null +++ b/test/units/sched_rr_change.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Change prio + +[Service] +ExecStart=/bin/true +CPUSchedulingPriority=1 +CPUSchedulingPriority=2 +CPUSchedulingPriority=99 +CPUSchedulingPolicy=rr diff --git a/test/units/sched_rr_ok.service b/test/units/sched_rr_ok.service new file mode 100644 index 0000000..00b9822 --- /dev/null +++ b/test/units/sched_rr_ok.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Default prio for RR + +[Service] +ExecStart=/bin/true +CPUSchedulingPolicy=rr diff --git a/test/units/shutdown.target b/test/units/shutdown.target new file mode 100644 index 0000000..582ae6b --- /dev/null +++ b/test/units/shutdown.target @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Shutdown +Documentation=man:systemd.special(7) +DefaultDependencies=no +RefuseManualStart=yes diff --git a/test/units/sockets.target b/test/units/sockets.target new file mode 100644 index 0000000..c6e20d7 --- /dev/null +++ b/test/units/sockets.target @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Sockets +Documentation=man:systemd.special(7) diff --git a/test/units/son.service b/test/units/son.service new file mode 100644 index 0000000..2059118 --- /dev/null +++ b/test/units/son.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Son Service + +[Service] +Slice=parent.slice +Type=oneshot +ExecStart=/bin/true +CPUShares=100 diff --git a/test/units/success-failure-test-failure.service b/test/units/success-failure-test-failure.service new file mode 100644 index 0000000..f4ce013 --- /dev/null +++ b/test/units/success-failure-test-failure.service @@ -0,0 +1,3 @@ +[Service] +Type=notify +ExecStart=bash -c "echo failure >> /tmp/success-failure-test-result && systemd-notify --ready && sleep infinity" diff --git a/test/units/success-failure-test-success.service b/test/units/success-failure-test-success.service new file mode 100644 index 0000000..8503c45 --- /dev/null +++ b/test/units/success-failure-test-success.service @@ -0,0 +1,3 @@ +[Service] +Type=notify +ExecStart=bash -c "echo success >> /tmp/success-failure-test-result && systemd-notify --ready && sleep infinity" diff --git a/test/units/success-failure-test.service b/test/units/success-failure-test.service new file mode 100644 index 0000000..f66ff6c --- /dev/null +++ b/test/units/success-failure-test.service @@ -0,0 +1,9 @@ +[Unit] +OnSuccess=success-failure-test-success.service +OnFailure=success-failure-test-failure.service + +[Service] +Type=notify +Restart=always +ExecStart=bash -c 'test -f /tmp/success-failure-test-ran && touch /tmp/success-failure-test-ran2 && systemd-notify --ready && sleep infinity' +ExecStopPost=touch /tmp/success-failure-test-ran diff --git a/test/units/sysinit.target b/test/units/sysinit.target new file mode 100644 index 0000000..eed3d16 --- /dev/null +++ b/test/units/sysinit.target @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Initialization +Documentation=man:systemd.special(7) +Conflicts=emergency.service emergency.target +Wants=local-fs.target swap.target +After=local-fs.target swap.target emergency.service emergency.target diff --git a/test/units/test-control.sh b/test/units/test-control.sh new file mode 100644 index 0000000..0a1611b --- /dev/null +++ b/test/units/test-control.sh @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck shell=bash + +if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then + echo >&2 "This file should not be executed directly" + exit 1 +fi + +declare -i _CHILD_PID=0 +_PASSED_TESTS=() + +# Like trap, but passes the signal name as the first argument +_trap_with_sig() { + local fun="${1:?}" + local sig + shift + + for sig in "$@"; do + # shellcheck disable=SC2064 + trap "$fun $sig" "$sig" + done +} + +# Propagate the caught signal to the current child process +_handle_signal() { + local sig="${1:?}" + + if [[ $_CHILD_PID -gt 0 ]]; then + echo "Propagating signal $sig to child process $_CHILD_PID" + kill -s "$sig" "$_CHILD_PID" + fi +} + +# In order to make the _handle_signal() stuff above work, we have to execute +# each script asynchronously, since bash won't execute traps until the currently +# executed command finishes. This, however, introduces another issue regarding +# how bash's wait works. Quoting: +# +# When bash is waiting for an asynchronous command via the wait builtin, +# the reception of a signal for which a trap has been set will cause the wait +# builtin to return immediately with an exit status greater than 128, +# immediately after which the trap is executed. +# +# In other words - every time we propagate a signal, wait returns with +# 128+signal, so we have to wait again - repeat until the process dies. +_wait_harder() { + local pid="${1:?}" + + while kill -0 "$pid" &>/dev/null; do + wait "$pid" || : + done + + wait "$pid" +} + +_show_summary() {( + set +x + + if [[ ${#_PASSED_TESTS[@]} -eq 0 ]]; then + echo >&2 "No tests were executed, this is most likely an error" + exit 1 + fi + + printf "PASSED TESTS: %3d:\n" "${#_PASSED_TESTS[@]}" + echo "------------------" + for t in "${_PASSED_TESTS[@]}"; do + echo "$t" + done +)} + +# Like run_subtests, but propagate specified signals to the subtest script +run_subtests_with_signals() { + local subtests=("${0%.sh}".*.sh) + local subtest + + if [[ "${#subtests[@]}" -eq 0 ]]; then + echo >&2 "No subtests found for file $0" + exit 1 + fi + + if [[ "$#" -eq 0 ]]; then + echo >&2 "No signals to propagate were specified" + exit 1 + fi + + _trap_with_sig _handle_signal "$@" + + for subtest in "${subtests[@]}"; do + if [[ -n "${TEST_MATCH_SUBTEST:-}" ]] && ! [[ "$subtest" =~ $TEST_MATCH_SUBTEST ]]; then + echo "Skipping $subtest (not matching '$TEST_MATCH_SUBTEST')" + continue + fi + + : "--- $subtest BEGIN ---" + SECONDS=0 + "./$subtest" & + _CHILD_PID=$! + if ! _wait_harder "$_CHILD_PID"; then + echo "Subtest $subtest failed" + return 1 + fi + + _PASSED_TESTS+=("$subtest") + : "--- $subtest END (${SECONDS}s) ---" + done + + _show_summary +} + +# Run all subtests (i.e. files named as testsuite-<testid>.<subtest_name>.sh) +run_subtests() { + local subtests=("${0%.sh}".*.sh) + local subtest + + if [[ "${#subtests[@]}" -eq 0 ]]; then + echo >&2 "No subtests found for file $0" + exit 1 + fi + + for subtest in "${subtests[@]}"; do + if [[ -n "${TEST_MATCH_SUBTEST:-}" ]] && ! [[ "$subtest" =~ $TEST_MATCH_SUBTEST ]]; then + echo "Skipping $subtest (not matching '$TEST_MATCH_SUBTEST')" + continue + fi + + : "--- $subtest BEGIN ---" + SECONDS=0 + if ! "./$subtest"; then + echo "Subtest $subtest failed" + return 1 + fi + + _PASSED_TESTS+=("$subtest") + : "--- $subtest END (${SECONDS}s) ---" + done + + _show_summary +} + +# Run all test cases (i.e. functions prefixed with testcase_ in the current namespace) +run_testcases() { + local testcase testcases + + # Create a list of all functions prefixed with testcase_ + mapfile -t testcases < <(declare -F | awk '$3 ~ /^testcase_/ {print $3;}') + + if [[ "${#testcases[@]}" -eq 0 ]]; then + echo >&2 "No test cases found, this is most likely an error" + exit 1 + fi + + for testcase in "${testcases[@]}"; do + if [[ -n "${TEST_MATCH_TESTCASE:-}" ]] && ! [[ "$testcase" =~ $TEST_MATCH_TESTCASE ]]; then + echo "Skipping $testcase (not matching '$TEST_MATCH_TESTCASE')" + continue + fi + + : "+++ $testcase BEGIN +++" + # Note: the subshell here is used purposefully, otherwise we might + # unexpectedly inherit a RETURN trap handler from the called + # function and call it for the second time once we return, + # causing a "double-free" + ("$testcase") + : "+++ $testcase END +++" + done +} diff --git a/test/units/testsuite-01.service b/test/units/testsuite-01.service new file mode 100644 index 0000000..9074e09 --- /dev/null +++ b/test/units/testsuite-01.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-01-BASIC +After=multi-user.target +Wants=systemd-resolved.service systemd-networkd.service + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-01.sh b/test/units/testsuite-01.sh new file mode 100755 index 0000000..870b62d --- /dev/null +++ b/test/units/testsuite-01.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Check if the colored --version output behaves correctly +SYSTEMD_COLORS=256 systemctl --version + +# Check if we properly differentiate between a full systemd setup and a "light" +# version of it that's done during daemon-reexec +# +# See: https://github.com/systemd/systemd/issues/27106 +if systemd-detect-virt -q --container; then + # We initialize /run/systemd/container only during a full setup + test -e /run/systemd/container + cp -afv /run/systemd/container /tmp/container + rm -fv /run/systemd/container + systemctl daemon-reexec + test ! -e /run/systemd/container + cp -afv /tmp/container /run/systemd/container +else + # We bring the loopback netdev up only during a full setup, so it should + # not get brought back up during reexec if we disable it beforehand + [[ "$(ip -o link show lo)" =~ LOOPBACK,UP ]] + ip link set lo down + [[ "$(ip -o link show lo)" =~ state\ DOWN ]] + systemctl daemon-reexec + [[ "$(ip -o link show lo)" =~ state\ DOWN ]] + ip link set lo up + + # We also disable coredumps only during a full setup + sysctl -w kernel.core_pattern=dont-overwrite-me + systemctl daemon-reexec + diff <(echo dont-overwrite-me) <(sysctl --values kernel.core_pattern) +fi + +# Collect failed units & do one daemon-reload to a basic sanity check +systemctl --state=failed --no-legend --no-pager | tee /failed +test ! -s /failed +systemctl daemon-reload + +# Check that the early setup is actually skipped on reexec. +# If the early setup is done more than once, then several timestamps, +# e.g. SecurityStartTimestamp, are re-initialized, and causes an ABRT +# of systemd-analyze blame. See issue #27187. +systemd-analyze blame + +# Test for 'systemd-update-utmp runlevel' vs 'systemctl daemon-reexec'. +# See issue #27163. +# shellcheck disable=SC2034 +for _ in {0..10}; do + systemctl daemon-reexec & + pid_reexec=$! + # shellcheck disable=SC2034 + for _ in {0..10}; do + SYSTEMD_LOG_LEVEL=debug /usr/lib/systemd/systemd-update-utmp runlevel + done + wait "$pid_reexec" +done + +touch /testok diff --git a/test/units/testsuite-02.service b/test/units/testsuite-02.service new file mode 100644 index 0000000..dea2c4f --- /dev/null +++ b/test/units/testsuite-02.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-02-UNITTESTS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-02.sh b/test/units/testsuite-02.sh new file mode 100755 index 0000000..2a3cb08 --- /dev/null +++ b/test/units/testsuite-02.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if ! systemd-detect-virt -qc && [[ "${TEST_CMDLINE_NEWLINE:-}" != bar ]]; then + cat /proc/cmdline + echo >&2 "Expected TEST_CMDLINE_NEWLINE=bar from the kernel command line" + exit 1 +fi + +# If we're running with TEST_PREFER_NSPAWN=1 limit the set of tests we run +# in QEMU to only those that can't run in a container to avoid running +# the same tests again in a, most likely, very slow environment +if ! systemd-detect-virt -qc && [[ "${TEST_PREFER_NSPAWN:-0}" -ne 0 ]]; then + TESTS_GLOB="test-loop-block" +else + TESTS_GLOB=${TESTS_GLOB:-test-*} +fi + +NPROC=$(nproc) +MAX_QUEUE_SIZE=${NPROC:-2} +mapfile -t TEST_LIST < <(find /usr/lib/systemd/tests/unit-tests/ -maxdepth 1 -type f -name "${TESTS_GLOB}") + +# Reset state +rm -fv /failed /skipped /testok + +if ! systemd-detect-virt -qc; then + # Make sure ping works for unprivileged users (for test-bpf-firewall) + sysctl net.ipv4.ping_group_range="0 2147483647" +fi + +# Check & report test results +# Arguments: +# $1: test path +# $2: test exit code +report_result() { + if [[ $# -ne 2 ]]; then + echo >&2 "check_result: missing arguments" + exit 1 + fi + + local name="${1##*/}" + local ret=$2 + + if [[ $ret -ne 0 && $ret != 77 && $ret != 127 ]]; then + echo "$name failed with $ret" + echo "$name" >>/failed-tests + { + echo "--- $name begin ---" + cat "/$name.log" + echo "--- $name end ---" + } >>/failed + elif [[ $ret == 77 || $ret == 127 ]]; then + echo "$name skipped" + echo "$name" >>/skipped-tests + { + echo "--- $name begin ---" + cat "/$name.log" + echo "--- $name end ---" + } >>/skipped + else + echo "$name OK" + echo "$name" >>/testok + fi +} + +set +x +# Associative array for running tasks, where running[test-path]=PID +declare -A running=() +for task in "${TEST_LIST[@]}"; do + # If there's MAX_QUEUE_SIZE running tasks, keep checking the running queue + # until one of the tasks finishes, so we can replace it. + while [[ ${#running[@]} -ge $MAX_QUEUE_SIZE ]]; do + for key in "${!running[@]}"; do + if ! kill -0 "${running[$key]}" &>/dev/null; then + # Task has finished, report its result and drop it from the queue + wait "${running[$key]}" && ec=0 || ec=$? + report_result "$key" "$ec" + unset "running[$key]" + # Break from inner for loop and outer while loop to skip + # the sleep below when we find a free slot in the queue + break 2 + fi + done + + # Precisely* calculated constant to keep the spinlock from burning the CPU(s) + sleep 0.01 + done + + if [[ -x $task ]]; then + echo "Executing test '$task'" + log_file="/${task##*/}.log" + $task &>"$log_file" & + running[$task]=$! + fi +done + +# Wait for remaining running tasks +for key in "${!running[@]}"; do + echo "Waiting for test '$key' to finish" + wait "${running[$key]}" && ec=0 || ec=$? + report_result "$key" "$ec" + unset "running[$key]" +done + +set -x + +# Test logs are sometimes lost, as the system shuts down immediately after +journalctl --sync + +test ! -s /failed +touch /testok diff --git a/test/units/testsuite-03.service b/test/units/testsuite-03.service new file mode 100644 index 0000000..836f962 --- /dev/null +++ b/test/units/testsuite-03.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-03-JOBS +After=multi-user.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-03.sh b/test/units/testsuite-03.sh new file mode 100755 index 0000000..e3567c2 --- /dev/null +++ b/test/units/testsuite-03.sh @@ -0,0 +1,169 @@ +#!/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 + +# Simple test for that daemon-reexec works in container. +# See: https://github.com/systemd/systemd/pull/23883 +systemctl daemon-reexec + +# Test merging of a --job-mode=ignore-dependencies job into a previously +# installed job. + +systemctl start --no-block hello-after-sleep.target + +systemctl list-jobs >/root/list-jobs.txt +until grep 'sleep\.service.*running' /root/list-jobs.txt; do + systemctl list-jobs >/root/list-jobs.txt +done + +grep 'hello\.service.*waiting' /root/list-jobs.txt + +# This is supposed to finish quickly, not wait for sleep to finish. +START_SEC=$(date -u '+%s') +systemctl start --job-mode=ignore-dependencies hello +END_SEC=$(date -u '+%s') +ELAPSED=$((END_SEC-START_SEC)) + +test "$ELAPSED" -lt 3 + +# sleep should still be running, hello not. +systemctl list-jobs >/root/list-jobs.txt +grep 'sleep\.service.*running' /root/list-jobs.txt +grep 'hello\.service' /root/list-jobs.txt && exit 1 +systemctl stop sleep.service hello-after-sleep.target + +# Some basic testing that --show-transaction does something useful +(! systemctl is-active systemd-importd) +systemctl -T start systemd-importd +systemctl is-active systemd-importd +systemctl --show-transaction stop systemd-importd +(! systemctl is-active systemd-importd) + +# Test for a crash when enqueuing a JOB_NOP when other job already exists +systemctl start --no-block hello-after-sleep.target +# hello.service should still be waiting, so these try-restarts will collapse +# into NOPs. +systemctl try-restart --job-mode=fail hello.service +systemctl try-restart hello.service +systemctl stop hello.service sleep.service hello-after-sleep.target + +# TODO: add more job queueing/merging tests here. + +# Test that restart propagates to activating units +systemctl -T --no-block start always-activating.service +systemctl list-jobs | grep 'always-activating.service' +ACTIVATING_ID_PRE=$(systemctl show -P InvocationID always-activating.service) +systemctl -T start always-activating.socket # Wait for the socket to come up +systemctl -T restart always-activating.socket +ACTIVATING_ID_POST=$(systemctl show -P InvocationID always-activating.service) +[ "$ACTIVATING_ID_PRE" != "$ACTIVATING_ID_POST" ] || exit 1 + +# Test for irreversible jobs +systemctl start unstoppable.service + +# This is expected to fail with 'job cancelled' +systemctl stop unstoppable.service && exit 1 +# But this should succeed +systemctl stop --job-mode=replace-irreversibly unstoppable.service + +# We're going to shutdown soon. Let's see if it succeeds when +# there's an active service that tries to be unstoppable. +# Shutdown of the container/VM will hang if not. +systemctl start unstoppable.service + +# Test waiting for a started units to terminate again +cat <<EOF >/run/systemd/system/wait2.service +[Unit] +Description=Wait for 2 seconds +[Service] +ExecStart=/bin/sh -ec 'sleep 2' +EOF +cat <<EOF >/run/systemd/system/wait5fail.service +[Unit] +Description=Wait for 5 seconds and fail +[Service] +ExecStart=/bin/sh -ec 'sleep 5; false' +EOF + +# wait2 succeeds +START_SEC=$(date -u '+%s') +systemctl start --wait wait2.service +END_SEC=$(date -u '+%s') +ELAPSED=$((END_SEC-START_SEC)) +[[ "$ELAPSED" -ge 2 ]] && [[ "$ELAPSED" -le 4 ]] || exit 1 + +# wait5fail fails, so systemctl should fail +START_SEC=$(date -u '+%s') +(! systemctl start --wait wait2.service wait5fail.service) +END_SEC=$(date -u '+%s') +ELAPSED=$((END_SEC-START_SEC)) +[[ "$ELAPSED" -ge 5 ]] && [[ "$ELAPSED" -le 7 ]] || exit 1 + +# Test time-limited scopes +START_SEC=$(date -u '+%s') +set +e +systemd-run --scope --property=RuntimeMaxSec=3s sleep 10 +RESULT=$? +END_SEC=$(date -u '+%s') +ELAPSED=$((END_SEC-START_SEC)) +[[ "$ELAPSED" -ge 3 ]] && [[ "$ELAPSED" -le 5 ]] || exit 1 +[[ "$RESULT" -ne 0 ]] || exit 1 + +# Test transactions with cycles +# Provides coverage for issues like https://github.com/systemd/systemd/issues/26872 +for i in {0..19}; do + cat >"/run/systemd/system/transaction-cycle$i.service" <<EOF +[Unit] +After=transaction-cycle$(((i + 1) % 20)).service +Requires=transaction-cycle$(((i + 1) % 20)).service + +[Service] +ExecStart=true +EOF +done +systemctl daemon-reload +for i in {0..19}; do + systemctl start "transaction-cycle$i.service" +done + +# Test PropagatesStopTo= when restart (issue #26839) +systemctl start propagatestopto-and-pullin.target +systemctl --quiet is-active propagatestopto-and-pullin.target + +systemctl restart propagatestopto-and-pullin.target +systemctl --quiet is-active propagatestopto-and-pullin.target +systemctl --quiet is-active sleep-infinity-simple.service + +systemctl start propagatestopto-only.target +systemctl --quiet is-active propagatestopto-only.target +systemctl --quiet is-active sleep-infinity-simple.service + +systemctl restart propagatestopto-only.target +assert_rc 3 systemctl --quiet is-active sleep-infinity-simple.service + +systemctl start propagatesstopto-indirect.target propagatestopto-and-pullin.target +systemctl --quiet is-active propagatestopto-indirect.target +systemctl --quiet is-active propagatestopto-and-pullin.target + +systemctl restart propagatestopto-indirect.target +assert_rc 3 systemctl --quiet is-active propagatestopto-and-pullin.target +assert_rc 3 systemctl --quiet is-active sleep-infinity-simple.service + +# Test restart mode direct +systemctl start succeeds-on-restart-restartdirect.target +assert_rc 0 systemctl --quiet is-active succeeds-on-restart-restartdirect.target + +systemctl start fails-on-restart-restartdirect.target || : +assert_rc 3 systemctl --quiet is-active fails-on-restart-restartdirect.target + +systemctl start succeeds-on-restart.target || : +assert_rc 3 systemctl --quiet is-active succeeds-on-restart.target + +systemctl start fails-on-restart.target || : +assert_rc 3 systemctl --quiet is-active fails-on-restart.target + +touch /testok diff --git a/test/units/testsuite-04.LogFilterPatterns.sh b/test/units/testsuite-04.LogFilterPatterns.sh new file mode 100755 index 0000000..2192e84 --- /dev/null +++ b/test/units/testsuite-04.LogFilterPatterns.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# This fails due to https://github.com/systemd/systemd/issues/30886 +# but it is too complex and risky to backport, so disable the test +exit 0 + +# shellcheck source=test/units/util.sh + . "$(dirname "$0")"/util.sh + +add_logs_filtering_override() { + local unit="${1:?}" + local override_name="${2:?}" + local log_filter="${3:-}" + + mkdir -p "/run/systemd/system/$unit.d/" + echo -ne "[Service]\nLogFilterPatterns=$log_filter" >"/run/systemd/system/$unit.d/$override_name.conf" + systemctl daemon-reload +} + +run_service_and_fetch_logs() { + local unit="${1:?}" + local start end + + start="$(date '+%Y-%m-%d %T.%6N')" + systemctl restart "$unit" + sleep .5 + journalctl --sync + end="$(date '+%Y-%m-%d %T.%6N')" + + journalctl -q -u "$unit" -S "$start" -U "$end" -p notice + systemctl stop "$unit" +} + +if cgroupfs_supports_user_xattrs; then + # Accept all log messages + add_logs_filtering_override "logs-filtering.service" "00-reset" "" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + add_logs_filtering_override "logs-filtering.service" "01-allow-all" ".*" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Discard all log messages + add_logs_filtering_override "logs-filtering.service" "02-discard-all" "~.*" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Accept all test messages + add_logs_filtering_override "logs-filtering.service" "03-reset" "" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Discard all test messages + add_logs_filtering_override "logs-filtering.service" "04-discard-gg" "~.*gg.*" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Deny filter takes precedence + add_logs_filtering_override "logs-filtering.service" "05-allow-all-but-too-late" ".*" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Use tilde in a deny pattern + add_logs_filtering_override "logs-filtering.service" "06-reset" "" + add_logs_filtering_override "logs-filtering.service" "07-prevent-tilde" "~~more~" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Only allow a pattern that won't be matched + add_logs_filtering_override "logs-filtering.service" "08-reset" "" + add_logs_filtering_override "logs-filtering.service" "09-allow-only-non-existing" "non-existing string" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Allow a pattern starting with a tilde + add_logs_filtering_override "logs-filtering.service" "10-allow-with-escape-char" "\\\\x7emore~" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + add_logs_filtering_override "logs-filtering.service" "11-reset" "" + add_logs_filtering_override "logs-filtering.service" "12-allow-with-spaces" "foo bar" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + add_logs_filtering_override "delegated-cgroup-filtering.service" "00-allow-all" ".*" + [[ -n $(run_service_and_fetch_logs "delegated-cgroup-filtering.service") ]] + + add_logs_filtering_override "delegated-cgroup-filtering.service" "01-discard-hello" "~hello" + [[ -z $(run_service_and_fetch_logs "delegated-cgroup-filtering.service") ]] + + rm -rf /run/systemd/system/{logs-filtering,delegated-cgroup-filtering}.service.d +fi diff --git a/test/units/testsuite-04.SYSTEMD_JOURNAL_COMPRESS.sh b/test/units/testsuite-04.SYSTEMD_JOURNAL_COMPRESS.sh new file mode 100755 index 0000000..96d096d --- /dev/null +++ b/test/units/testsuite-04.SYSTEMD_JOURNAL_COMPRESS.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# https://bugzilla.redhat.com/show_bug.cgi?id=2183546 +mkdir /run/systemd/system/systemd-journald.service.d +MACHINE_ID="$(</etc/machine-id)" + +# Reset the start-limit counters, as we're going to restart journald a couple of times +systemctl reset-failed systemd-journald.service + +for c in NONE XZ LZ4 ZSTD; do + cat >/run/systemd/system/systemd-journald.service.d/compress.conf <<EOF +[Service] +Environment=SYSTEMD_JOURNAL_COMPRESS=${c} +EOF + systemctl daemon-reload + systemctl restart systemd-journald.service + journalctl --rotate + + ID="$(systemd-id128 new)" + systemd-cat -t "$ID" /bin/bash -c "for ((i=0;i<100;i++)); do echo -n hoge with ${c}; done; echo" + journalctl --sync + timeout 10 bash -c "until SYSTEMD_LOG_LEVEL=debug journalctl --verify --quiet --file /var/log/journal/$MACHINE_ID/system.journal 2>&1 | grep -q -F 'compress=${c}'; do sleep .5; done" + + # $SYSTEMD_JOURNAL_COMPRESS= also works for journal-remote + if [[ -x /usr/lib/systemd/systemd-journal-remote ]]; then + for cc in NONE XZ LZ4 ZSTD; do + rm -f /tmp/foo.journal + SYSTEMD_JOURNAL_COMPRESS="${cc}" /usr/lib/systemd/systemd-journal-remote --split-mode=none -o /tmp/foo.journal --getter="journalctl -b -o export -t $ID" + SYSTEMD_LOG_LEVEL=debug journalctl --verify --quiet --file /tmp/foo.journal 2>&1 | grep -q -F "compress=${cc}" + journalctl -t "$ID" -o cat --file /tmp/foo.journal | grep -q -F "hoge with ${c}" + done + fi +done + +rm /run/systemd/system/systemd-journald.service.d/compress.conf +systemctl daemon-reload +systemctl restart systemd-journald.service +systemctl reset-failed systemd-journald.service +journalctl --rotate diff --git a/test/units/testsuite-04.bsod.sh b/test/units/testsuite-04.bsod.sh new file mode 100755 index 0000000..30f0cb0 --- /dev/null +++ b/test/units/testsuite-04.bsod.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if systemd-detect-virt -cq; then + echo "This test requires a VM, skipping the test" + exit 0 +fi + +# shellcheck disable=SC2317 +at_exit() { + local EC=$? + + if [[ $EC -ne 0 ]] && [[ -e /tmp/console.dump ]]; then + cat /tmp/console.dump + fi + + if mountpoint -q /var/log/journal; then + journalctl --relinquish-var + umount /var/log/journal + journalctl --flush + fi + + return 0 +} + +vcs_dump_and_check() { + local expected_message="${1:?}" + + # It might take a while before the systemd-bsod stuff appears on the VCS, + # so try it a couple of times + for _ in {0..9}; do + setterm --term linux --dump --file /tmp/console.dump + if grep -aq "Press any key to exit" /tmp/console.dump && + grep -aq "$expected_message" /tmp/console.dump && + grep -aq "The current boot has failed" /tmp/console.dump; then + + return 0 + fi + + sleep .5 + done + + return 1 +} + +# Since systemd-bsod always fetches only the first emergency message from the +# current boot, let's temporarily overmount /var/log/journal with a tmpfs, +# as we're going to wipe it multiple times, but we need to keep the original +# journal intact for the other tests to work correctly. +trap at_exit EXIT +mount -t tmpfs tmpfs /var/log/journal +systemctl restart systemd-journald + +systemctl stop systemd-bsod + +# Since we just wiped the journal, there should be no emergency messages and +# systemd-bsod should be just a no-op +timeout 10s /usr/lib/systemd/systemd-bsod +setterm --term linux --dump --file /tmp/console.dump +(! grep "The current boot has failed" /tmp/console.dump) + +# systemd-bsod should pick up emergency messages only with UID=0, so let's check +# that as well +systemd-run --user --machine testuser@ --wait --pipe systemd-cat -p emerg echo "User emergency message" +systemd-cat -p emerg echo "Root emergency message" +journalctl --sync +# Set $SYSTEMD_COLORS so systemd-bsod also prints out the QR code +SYSTEMD_COLORS=256 /usr/lib/systemd/systemd-bsod & +PID=$! +vcs_dump_and_check "Root emergency message" +grep -aq "Scan the QR code" /tmp/console.dump +# TODO: check if systemd-bsod exits on a key press (didn't figure this one out yet) +kill $PID +timeout 10 bash -c "while kill -0 $PID; do sleep .5; done" + +# Wipe the journal +journalctl --vacuum-size=1 --rotate +(! journalctl -q -b -p emerg --grep .) + +# Check the systemd-bsod.service as well +# Note: the systemd-bsod.service unit has ConditionVirtualization=no, so let's +# temporarily override it just for the test +mkdir /run/systemd/system/systemd-bsod.service.d +printf '[Unit]\nConditionVirtualization=\n' >/run/systemd/system/systemd-bsod.service.d/99-override.conf +systemctl daemon-reload +systemctl start systemd-bsod +systemd-cat -p emerg echo "Service emergency message" +vcs_dump_and_check "Service emergency message" +systemctl stop systemd-bsod + +# Wipe the journal +journalctl --vacuum-size=1 --rotate +(! journalctl -q -b -p emerg --grep .) + +# Same as above, but make sure the service responds to signals even when there are +# no "emerg" messages, see systemd/systemd#30084 +(! systemctl is-active systemd-bsod) +systemctl start systemd-bsod +timeout 5s bash -xec 'until systemctl is-active systemd-bsod; do sleep .5; done' +timeout 5s systemctl stop systemd-bsod +timeout 5s bash -xec 'while systemctl is-active systemd-bsod; do sleep .5; done' diff --git a/test/units/testsuite-04.corrupted-journals.sh b/test/units/testsuite-04.corrupted-journals.sh new file mode 100755 index 0000000..2123b10 --- /dev/null +++ b/test/units/testsuite-04.corrupted-journals.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +JOURNAL_DIR="$(mktemp -d)" +REMOTE_OUT="$(mktemp -d)" +# tar on C8S doesn't support the --zstd option +unzstd --stdout "/test-journals/afl-corrupted-journals.tar.zst" | tar -xC "$JOURNAL_DIR/" +while read -r file; do + filename="${file##*/}" + unzstd "$file" -o "$JOURNAL_DIR/${filename%*.zst}" +done < <(find /test-journals/corrupted/ -name "*.zst") +# First, try each of them sequentially. Skip this part when running with plain +# QEMU, as it is excruciatingly slow +# Note: we care only about exit code 124 (timeout) and special bash exit codes +# >124 (like signals) +if [[ "$(systemd-detect-virt -v)" != "qemu" ]]; then + while read -r file; do + timeout 10 journalctl --file="$file" --boot >/dev/null || [[ $? -lt 124 ]] + timeout 10 journalctl --file="$file" --verify >/dev/null || [[ $? -lt 124 ]] + timeout 10 journalctl --file="$file" --output=export >/dev/null || [[ $? -lt 124 ]] + timeout 10 journalctl --file="$file" --fields >/dev/null || [[ $? -lt 124 ]] + timeout 10 journalctl --file="$file" --list-boots >/dev/null || [[ $? -lt 124 ]] + if [[ -x /usr/lib/systemd/systemd-journal-remote ]]; then + timeout 10 /usr/lib/systemd/systemd-journal-remote \ + --getter="journalctl --file=$file --output=export" \ + --split-mode=none \ + --output="$REMOTE_OUT/system.journal" || [[ $? -lt 124 ]] + timeout 10 journalctl --directory="$REMOTE_OUT" >/dev/null || [[ $? -lt 124 ]] + rm -f "$REMOTE_OUT"/* + fi + done < <(find "$JOURNAL_DIR" -type f) +fi +# And now all at once +timeout 30 journalctl --directory="$JOURNAL_DIR" --boot >/dev/null || [[ $? -lt 124 ]] +timeout 30 journalctl --directory="$JOURNAL_DIR" --verify >/dev/null || [[ $? -lt 124 ]] +timeout 30 journalctl --directory="$JOURNAL_DIR" --output=export >/dev/null || [[ $? -lt 124 ]] +timeout 30 journalctl --directory="$JOURNAL_DIR" --fields >/dev/null || [[ $? -lt 124 ]] +timeout 30 journalctl --directory="$JOURNAL_DIR" --list-boots >/dev/null || [[ $? -lt 124 ]] +if [[ -x /usr/lib/systemd/systemd-journal-remote ]]; then + timeout 30 /usr/lib/systemd/systemd-journal-remote \ + --getter="journalctl --directory=$JOURNAL_DIR --output=export" \ + --split-mode=none \ + --output="$REMOTE_OUT/system.journal" || [[ $? -lt 124 ]] + timeout 30 journalctl --directory="$REMOTE_OUT" >/dev/null || [[ $? -lt 124 ]] + rm -f "$REMOTE_OUT"/* +fi diff --git a/test/units/testsuite-04.fss.sh b/test/units/testsuite-04.fss.sh new file mode 100755 index 0000000..03351b8 --- /dev/null +++ b/test/units/testsuite-04.fss.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Forward Secure Sealing + +if ! journalctl --version | grep -qF +GCRYPT; then + echo "Built without gcrypt, skipping the FSS tests" + exit 0 +fi + +journalctl --force --setup-keys --interval=2 |& tee /tmp/fss +FSS_VKEY="$(sed -rn '/([a-f0-9]{6}\-){3}[a-f0-9]{6}\/[a-f0-9]+\-[a-f0-9]+/p' /tmp/fss)" +[[ -n "$FSS_VKEY" ]] + +# Generate some buzz in the journal and wait until the FSS key is changed +# at least once +systemd-cat cat /etc/os-release +sleep 4 +# Seal the journal +journalctl --rotate +# Verification should fail without a valid FSS key +(! journalctl --verify) +(! journalctl --verify --verify-key="") +(! journalctl --verify --verify-key="000000-000000-000000-000000/00000000-00000") +# FIXME: ignore --verify result until #27532 is resolved +journalctl --verify --verify-key="$FSS_VKEY" || : + +# Sealing + systemd-journal-remote +/usr/lib/systemd/systemd-journal-remote --getter="journalctl -n 5 -o export" \ + --split-mode=none \ + --seal=yes \ + --output=/tmp/sealed.journal +(! journalctl --file=/tmp/sealed.journal --verify) +(! journalctl --file=/tmp/sealed.journal --verify --verify-key="") +(! journalctl --file=/tmp/sealed.journal --verify --verify-key="000000-000000-000000-000000/00000000-00000") +# FIXME: ignore --verify result until #27532 is resolved +journalctl --file=/tmp/sealed.journal --verify --verify-key="$FSS_VKEY" || : +rm -f /tmp/sealed.journal + +# Return back to a journal without FSS +rm -fv "/var/log/journal/$(</etc/machine-id)/fss" +journalctl --rotate --vacuum-size=1 +# FIXME: ignore --verify result until #27532 is resolved +journalctl --verify || : diff --git a/test/units/testsuite-04.journal-append.sh b/test/units/testsuite-04.journal-append.sh new file mode 100755 index 0000000..35f9433 --- /dev/null +++ b/test/units/testsuite-04.journal-append.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# test-journal-append corrupts the journal file by flipping a bit at a given offset and +# following it by a write to check if we handle appending messages to corrupted journals +# gracefully + +TEST_JOURNAL_APPEND=/usr/lib/systemd/tests/unit-tests/manual/test-journal-append + +[[ -x "$TEST_JOURNAL_APPEND" ]] + +# Corrupt the first ~1024 bytes, this should be pretty quick +"$TEST_JOURNAL_APPEND" --sequential --start-offset=0 --iterations=350 --iteration-step=3 + +# Skip most of the test when running without acceleration, as it's excruciatingly slow +# (this shouldn't be an issue, as it should run in nspawn as well) +if ! [[ "$(systemd-detect-virt -v)" == "qemu" ]]; then + # Corrupt the beginning of every 1K block between 1K - 32K + for ((i = 1024; i <= (32 * 1024); i += 1024)); do + "$TEST_JOURNAL_APPEND" --sequential --start-offset="$i" --iterations=5 --iteration-step=13 + done + + # Corrupt the beginning of every 16K block between 32K - 128K + for ((i = (32 * 1024); i <= (256 * 1024); i += (16 * 1024))); do + "$TEST_JOURNAL_APPEND" --sequential --start-offset="$i" --iterations=5 --iteration-step=13 + done + + # Corrupt the beginning of every 128K block between 128K - 1M + for ((i = (128 * 1024); i <= (1 * 1024 * 1024); i += (128 * 1024))); do + "$TEST_JOURNAL_APPEND" --sequential --start-offset="$i" --iterations=5 --iteration-step=13 + done + + # And finally the beginning of every 1M block between 1M and 8M + for ((i = (1 * 1024 * 1024); i < (8 * 1024 * 1024); i += (1 * 1024 * 1024))); do + "$TEST_JOURNAL_APPEND" --sequential --start-offset="$i" --iterations=5 --iteration-step=13 + done + + if [[ "$(nproc)" -ge 2 ]]; then + # Try to corrupt random bytes throughout the journal + "$TEST_JOURNAL_APPEND" --iterations=25 + fi +else + "$TEST_JOURNAL_APPEND" --iterations=10 +fi diff --git a/test/units/testsuite-04.journal-gatewayd.sh b/test/units/testsuite-04.journal-gatewayd.sh new file mode 100755 index 0000000..5755ef1 --- /dev/null +++ b/test/units/testsuite-04.journal-gatewayd.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +# pipefail is disabled intentionally, as `curl | grep -q` is very SIGPIPE happy + +if [[ ! -x /usr/lib/systemd/systemd-journal-gatewayd ]]; then + echo "Built without systemd-journal-gatewayd support, skipping the test" + exit 0 +fi + +TEST_MESSAGE="-= This is a test message $RANDOM =-" +TEST_TAG="$(systemd-id128 new)" + +echo "$TEST_MESSAGE" | systemd-cat -t "$TEST_TAG" +journalctl --sync +TEST_CURSOR="$(journalctl -q -t "$TEST_TAG" -n 0 --show-cursor | awk '{ print $3; }')" +BOOT_CURSOR="$(journalctl -q -b -n 0 --show-cursor | awk '{ print $3; }')" + +/usr/lib/systemd/systemd-journal-gatewayd --version +/usr/lib/systemd/systemd-journal-gatewayd --help + +# Default configuration (HTTP, socket activated) +systemctl start systemd-journal-gatewayd.socket + +# /browse +# We should get redirected to /browse by default +curl -Lfs http://localhost:19531 | grep -qF "<title>Journal</title>" +curl -Lfs http://localhost:19531/browse | grep -qF "<title>Journal</title>" +(! curl -Lfs http://localhost:19531/foo/bar/baz) +(! curl -Lfs http://localhost:19531/foo/../../../bar/../baz) + +# /entries +# Accept: text/plain should be the default +curl -Lfs http://localhost:19531/entries | \ + grep -qE " $TEST_TAG\[[0-9]+\]: $TEST_MESSAGE" +curl -Lfs --header "Accept: text/plain" http://localhost:19531/entries | \ + grep -qE " $TEST_TAG\[[0-9]+\]: $TEST_MESSAGE" +curl -Lfs --header "Accept: application/json" http://localhost:19531/entries | \ + jq -se ".[] | select(.MESSAGE == \"$TEST_MESSAGE\")" +curl -Lfs --header "Accept: application/json" http://localhost:19531/entries?boot | \ + jq -se ".[] | select(.MESSAGE == \"$TEST_MESSAGE\")" +curl -Lfs --header "Accept: application/json" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \ + jq -se "length == 1 and select(.[].MESSAGE == \"$TEST_MESSAGE\")" +# Show 10 entries starting from $BOOT_CURSOR, skip the first 5 +curl -Lfs --header "Accept: application/json" --header "Range: entries=$BOOT_CURSOR:5:10" http://localhost:19531/entries | \ + jq -se "length == 10" +# Check if the specified cursor refers to an existing entry and return just that entry +curl -Lfs --header "Accept: application/json" --header "Range: entries=$TEST_CURSOR" http://localhost:19531/entries?discrete | \ + jq -se "length == 1 and select(.[].MESSAGE == \"$TEST_MESSAGE\")" +# No idea how to properly parse this (jq won't cut it), so let's at least do some sanity checks that every +# line is either empty or begins with data: +curl -Lfs --header "Accept: text/event-stream" http://localhost:19531/entries | \ + awk '!/^(data: \{.+\}|)$/ { exit 1; }' +# Same thing as journalctl --output=export +mkdir /tmp/remote-journal +curl -Lfs --header "Accept: application/vnd.fdo.journal" http://localhost:19531/entries | \ + /usr/lib/systemd/systemd-journal-remote --output=/tmp/remote-journal/system.journal --split-mode=none - +journalctl --directory=/tmp/remote-journal -t "$TEST_TAG" --grep "$TEST_MESSAGE" +rm -rf /tmp/remote-journal/* +# Let's do the same thing again, but let systemd-journal-remote spawn curl itself +/usr/lib/systemd/systemd-journal-remote --url=http://localhost:19531/entries \ + --output=/tmp/remote-journal/system.journal \ + --split-mode=none +journalctl --directory=/tmp/remote-journal -t "$TEST_TAG" --grep "$TEST_MESSAGE" +rm -rf /tmp/remote-journal + +# /machine +curl -Lfs http://localhost:19531/machine | jq + +# /fields +curl -Lfs http://localhost:19531/fields/MESSAGE | grep -qE -- "$TEST_MESSAGE" +curl -Lfs http://localhost:19531/fields/_TRANSPORT +(! curl -Lfs http://localhost:19531/fields) +(! curl -Lfs http://localhost:19531/fields/foo-bar-baz) + +systemctl stop systemd-journal-gatewayd.{socket,service} + +if ! command -v openssl >/dev/null; then + echo "openssl command not available, skipping the HTTPS tests" + exit 0 +fi + +# Generate a self-signed certificate for systemd-journal-gatewayd +# +# Note: older OpenSSL requires a config file with some extra options, unfortunately +cat >/tmp/openssl.conf <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = CZ +L = Brno +O = Foo +OU = Bar +CN = localhost +EOF +openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 7 \ + -config /tmp/openssl.conf \ + -keyout /tmp/key.pem -out /tmp/cert.pem +# Start HTTPS version of gatewayd via the systemd-socket-activate tool to give it some coverage as well +systemd-socket-activate --listen=19531 -- \ + /usr/lib/systemd/systemd-journal-gatewayd \ + --cert=/tmp/cert.pem \ + --key=/tmp/key.pem \ + --file="/var/log/journal/*/*.journal" & +GATEWAYD_PID=$! +sleep 1 + +# Do a limited set of tests, since the underlying code should be the same past the HTTPS transport +curl -Lfsk https://localhost:19531 | grep -qF "<title>Journal</title>" +curl -Lfsk https://localhost:19531/entries | \ + grep -qE " $TEST_TAG\[[0-9]+\]: $TEST_MESSAGE" +curl -Lfsk --header "Accept: application/json" https://localhost:19531/entries | \ + jq -se ".[] | select(.MESSAGE == \"$TEST_MESSAGE\")" +curl -Lfsk https://localhost:19531/machine | jq +curl -Lfsk https://localhost:19531/fields/_TRANSPORT + +kill "$GATEWAYD_PID" diff --git a/test/units/testsuite-04.journal-remote.sh b/test/units/testsuite-04.journal-remote.sh new file mode 100755 index 0000000..c7b99b1 --- /dev/null +++ b/test/units/testsuite-04.journal-remote.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +if [[ ! -x /usr/lib/systemd/systemd-journal-remote || ! -x /usr/lib/systemd/systemd-journal-upload ]]; then + echo "Built without systemd-journal-remote/upload support, skipping the test" + exit 0 +fi + +if ! command -v openssl >/dev/null; then + echo "openssl command not available, skipping the tests" + exit 0 +fi + +at_exit() { + set +e + + systemctl stop systemd-journal-upload + systemctl stop systemd-journal-remote.{socket,service} + # Remove any remote journals on exit, so we don't try to export them together + # with the local journals, causing a mess + rm -rf /var/log/journal/remote +} + +trap at_exit EXIT + +TEST_MESSAGE="-= This is a test message $RANDOM =-" +TEST_TAG="$(systemd-id128 new)" + +echo "$TEST_MESSAGE" | systemd-cat -t "$TEST_TAG" +journalctl --sync + +/usr/lib/systemd/systemd-journal-remote --version +/usr/lib/systemd/systemd-journal-remote --help +/usr/lib/systemd/systemd-journal-upload --version +/usr/lib/systemd/systemd-journal-upload --help + +# Generate a self-signed certificate for systemd-journal-remote +# +# Note: older OpenSSL requires a config file with some extra options, unfortunately +# Note2: /run here is used on purpose, since the systemd-journal-remote service uses PrivateTmp=yes +mkdir -p /run/systemd/journal-remote-tls +cat >/tmp/openssl.conf <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = CZ +L = Brno +O = Foo +OU = Bar +CN = localhost +EOF +openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 7 \ + -config /tmp/openssl.conf \ + -keyout /run/systemd/journal-remote-tls/key.pem \ + -out /run/systemd/journal-remote-tls/cert.pem +chown -R systemd-journal-remote /run/systemd/journal-remote-tls + +# Configure journal-upload to upload journals to journal-remote without client certificates +mkdir -p /run/systemd/journal-{remote,upload}.conf.d +cat >/run/systemd/journal-remote.conf.d/99-test.conf <<EOF +[Remote] +SplitMode=host +ServerKeyFile=/run/systemd/journal-remote-tls/key.pem +ServerCertificateFile=/run/systemd/journal-remote-tls/cert.pem +TrustedCertificateFile=- +EOF +cat >/run/systemd/journal-upload.conf.d/99-test.conf <<EOF +[Upload] +URL=https://localhost:19532 +ServerKeyFile=- +ServerCertificateFile=- +TrustedCertificateFile=- +EOF +systemd-analyze cat-config systemd/journal-remote.conf +systemd-analyze cat-config systemd/journal-upload.conf + +systemctl restart systemd-journal-remote.socket +systemctl restart systemd-journal-upload +timeout 15 bash -xec 'until systemctl -q is-active systemd-journal-remote.service; do sleep 1; done' +systemctl status systemd-journal-{remote,upload} + +# It may take a bit until the whole journal is transferred +timeout 30 bash -xec "until journalctl --directory=/var/log/journal/remote --identifier='$TEST_TAG' --grep='$TEST_MESSAGE'; do sleep 1; done" + +systemctl stop systemd-journal-upload +systemctl stop systemd-journal-remote.{socket,service} +rm -rf /var/log/journal/remote/* + +# Now let's do the same, but with a full PKI setup +# +# journal-upload keeps the cursor of the last uploaded message, so let's send a fresh one +echo "$TEST_MESSAGE" | systemd-cat -t "$TEST_TAG" +journalctl --sync + +mkdir /run/systemd/remote-pki +cat >/run/systemd/remote-pki/ca.conf <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = CZ +L = Brno +O = Foo +OU = Bar +CN = Test CA + +[ v3_ca ] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +basicConstraints = CA:true +EOF +cat >/run/systemd/remote-pki/client.conf <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = CZ +L = Brno +O = Foo +OU = Bar +CN = Test Client +EOF +cat >/run/systemd/remote-pki/server.conf <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = CZ +L = Brno +O = Foo +OU = Bar +CN = localhost +EOF +# Generate a dummy CA +openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 7 \ + -extensions v3_ca \ + -config /run/systemd/remote-pki/ca.conf \ + -keyout /run/systemd/remote-pki/ca.key \ + -out /run/systemd/remote-pki/ca.crt +openssl x509 -in /run/systemd/remote-pki/ca.crt -noout -text +echo 01 >/run/systemd/remote-pki/ca.srl +# Generate a client key and signing request +openssl req -nodes -newkey rsa:2048 -sha256 \ + -config /run/systemd/remote-pki/client.conf \ + -keyout /run/systemd/remote-pki/client.key \ + -out /run/systemd/remote-pki/client.csr +# Sign the request with the CA key +openssl x509 -req -days 7 \ + -in /run/systemd/remote-pki/client.csr \ + -CA /run/systemd/remote-pki/ca.crt \ + -CAkey /run/systemd/remote-pki/ca.key \ + -out /run/systemd/remote-pki/client.crt +# And do the same for the server +openssl req -nodes -newkey rsa:2048 -sha256 \ + -config /run/systemd/remote-pki/server.conf \ + -keyout /run/systemd/remote-pki/server.key \ + -out /run/systemd/remote-pki/server.csr +openssl x509 -req -days 7 \ + -in /run/systemd/remote-pki/server.csr \ + -CA /run/systemd/remote-pki/ca.crt \ + -CAkey /run/systemd/remote-pki/ca.key \ + -out /run/systemd/remote-pki/server.crt +chown -R systemd-journal-remote:systemd-journal /run/systemd/remote-pki +chmod -R g+rwX /run/systemd/remote-pki + +# Reconfigure journal-upload/journal remote with the new keys +cat >/run/systemd/journal-remote.conf.d/99-test.conf <<EOF +[Remote] +SplitMode=host +ServerKeyFile=/run/systemd/remote-pki/server.key +ServerCertificateFile=/run/systemd/remote-pki/server.crt +TrustedCertificateFile=/run/systemd/remote-pki/ca.crt +EOF +cat >/run/systemd/journal-upload.conf.d/99-test.conf <<EOF +[Upload] +URL=https://localhost:19532 +ServerKeyFile=/run/systemd/remote-pki/client.key +ServerCertificateFile=/run/systemd/remote-pki/client.crt +TrustedCertificateFile=/run/systemd/remote-pki/ca.crt +EOF +systemd-analyze cat-config systemd/journal-remote.conf +systemd-analyze cat-config systemd/journal-upload.conf + +systemctl restart systemd-journal-remote.socket +systemctl restart systemd-journal-upload +timeout 15 bash -xec 'until systemctl -q is-active systemd-journal-remote.service; do sleep 1; done' +systemctl status systemd-journal-{remote,upload} + +# It may take a bit until the whole journal is transferred +timeout 30 bash -xec "until journalctl --directory=/var/log/journal/remote --identifier='$TEST_TAG' --grep='$TEST_MESSAGE'; do sleep 1; done" + +systemctl stop systemd-journal-upload +systemctl stop systemd-journal-remote.{socket,service} + +# Let's test if journal-remote refuses connection from journal-upload with invalid client certs +# +# We should end up with something like this: +# systemd-journal-remote[726]: Client is not authorized +# systemd-journal-upload[738]: Upload to https://localhost:19532/upload failed with code 401: +# systemd[1]: systemd-journal-upload.service: Main process exited, code=exited, status=1/FAILURE +# systemd[1]: systemd-journal-upload.service: Failed with result 'exit-code'. +# +cat >/run/systemd/journal-upload.conf.d/99-test.conf <<EOF +[Upload] +URL=https://localhost:19532 +ServerKeyFile=/run/systemd/journal-remote-tls/key.pem +ServerCertificateFile=/run/systemd/journal-remote-tls/cert.pem +TrustedCertificateFile=/run/systemd/remote-pki/ca.crt +EOF +systemd-analyze cat-config systemd/journal-upload.conf +mkdir -p /run/systemd/system/systemd-journal-upload.service.d +cat >/run/systemd/system/systemd-journal-upload.service.d/99-test.conf <<EOF +[Service] +Restart=no +EOF +systemctl daemon-reload +chgrp -R systemd-journal /run/systemd/journal-remote-tls +chmod -R g+rwX /run/systemd/journal-remote-tls + +systemctl restart systemd-journal-upload +timeout 10 bash -xec 'while [[ "$(systemctl show -P ActiveState systemd-journal-upload)" != failed ]]; do sleep 1; done' +(! systemctl status systemd-journal-upload) diff --git a/test/units/testsuite-04.journal.sh b/test/units/testsuite-04.journal.sh new file mode 100755 index 0000000..c19cd12 --- /dev/null +++ b/test/units/testsuite-04.journal.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# This fails due to https://github.com/systemd/systemd/issues/30886 +# but it is too complex and risky to backport, so disable the test +exit 0 + +# Rotation/flush test, see https://github.com/systemd/systemd/issues/19895 +journalctl --relinquish-var +[[ "$(systemd-detect-virt -v)" == "qemu" ]] && ITERATIONS=10 || ITERATIONS=50 +for ((i = 0; i < ITERATIONS; i++)); do + dd if=/dev/urandom bs=1M count=1 | base64 | systemd-cat +done +journalctl --rotate +# Let's test varlinkctl a bit, i.e. implement the equivalent of 'journalctl --rotate' via varlinkctl +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Rotate '{}' +journalctl --flush +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.FlushToVar '{}' +journalctl --sync +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Synchronize '{}' +journalctl --rotate --vacuum-size=8M + +# Reset the ratelimit buckets for the subsequent tests below. +systemctl restart systemd-journald + +# Test stdout stream +write_and_match() { + local input="${1:?}" + local expected="${2?}" + local id + shift 2 + + id="$(systemd-id128 new)" + echo -ne "$input" | systemd-cat -t "$id" "$@" + journalctl --sync + diff <(echo -ne "$expected") <(journalctl -b -o cat -t "$id") +} +# Skip empty lines +write_and_match "\n\n\n" "" --level-prefix false +write_and_match "<5>\n<6>\n<7>\n" "" --level-prefix true +# Remove trailing spaces +write_and_match "Trailing spaces \t \n" "Trailing spaces\n" --level-prefix false +write_and_match "<5>Trailing spaces \t \n" "Trailing spaces\n" --level-prefix true +# Don't remove leading spaces +write_and_match " \t Leading spaces\n" " \t Leading spaces\n" --level-prefix false +write_and_match "<5> \t Leading spaces\n" " \t Leading spaces\n" --level-prefix true + +# --output-fields restricts output +ID="$(systemd-id128 new)" +echo -ne "foo" | systemd-cat -t "$ID" --level-prefix false +# Let's test varlinkctl a bit, i.e. implement the equivalent of 'journalctl --sync' via varlinkctl +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Synchronize '{}' +journalctl -b -o export --output-fields=MESSAGE,FOO --output-fields=PRIORITY,MESSAGE -t "$ID" >/tmp/output +[[ $(wc -l </tmp/output) -eq 9 ]] +grep -q '^__CURSOR=' /tmp/output +grep -q '^MESSAGE=foo$' /tmp/output +grep -q '^PRIORITY=6$' /tmp/output +(! grep '^FOO=' /tmp/output) +(! grep '^SYSLOG_FACILITY=' /tmp/output) + +# --truncate shows only first line, skip under asan due to logger +ID="$(systemd-id128 new)" +echo -e 'HEAD\nTAIL\nTAIL' | systemd-cat -t "$ID" +journalctl --sync +journalctl -b -t "$ID" | grep -q HEAD +journalctl -b -t "$ID" | grep -q TAIL +journalctl -b -t "$ID" --truncate-newline | grep -q HEAD +journalctl -b -t "$ID" --truncate-newline | grep -q -v TAIL + +# '-b all' negates earlier use of -b (-b and -m are otherwise exclusive) +journalctl -b -1 -b all -m >/dev/null + +# -b always behaves like -b0 +journalctl -q -b-1 -b0 | head -1 >/tmp/expected +journalctl -q -b-1 -b | head -1 >/tmp/output +diff /tmp/expected /tmp/output +# ... even when another option follows (both of these should fail due to -m) +{ journalctl -ball -b0 -m 2>&1 || :; } | head -1 >/tmp/expected +{ journalctl -ball -b -m 2>&1 || :; } | head -1 >/tmp/output +diff /tmp/expected /tmp/output + +# https://github.com/systemd/systemd/issues/13708 +ID=$(systemd-id128 new) +systemd-cat -t "$ID" bash -c 'echo parent; (echo child) & wait' & +PID=$! +wait $PID +journalctl --sync +# We can drop this grep when https://github.com/systemd/systemd/issues/13937 +# has a fix. +journalctl -b -o export -t "$ID" --output-fields=_PID | grep '^_PID=' >/tmp/output +[[ $(wc -l </tmp/output) -eq 2 ]] +grep -q "^_PID=$PID" /tmp/output +grep -vq "^_PID=$PID" /tmp/output + +# https://github.com/systemd/systemd/issues/15654 +ID=$(systemd-id128 new) +printf "This will\nusually fail\nand be truncated\n" >/tmp/expected +systemd-cat -t "$ID" /bin/sh -c 'env echo -n "This will";echo;env echo -n "usually fail";echo;env echo -n "and be truncated";echo;' +journalctl --sync +journalctl -b -o cat -t "$ID" >/tmp/output +diff /tmp/expected /tmp/output +[[ $(journalctl -b -o cat -t "$ID" --output-fields=_TRANSPORT | grep -Pc "^stdout$") -eq 3 ]] +[[ $(journalctl -b -o cat -t "$ID" --output-fields=_LINE_BREAK | grep -Pc "^pid-change$") -eq 3 ]] +[[ $(journalctl -b -o cat -t "$ID" --output-fields=_PID | sort -u | grep -c "^.*$") -eq 3 ]] +[[ $(journalctl -b -o cat -t "$ID" --output-fields=MESSAGE | grep -Pc "^(This will|usually fail|and be truncated)$") -eq 3 ]] + +# test that LogLevelMax can also suppress logging about services, not only by services +systemctl start silent-success +journalctl --sync +[[ -z "$(journalctl -b -q -u silent-success.service)" ]] + +# Exercise the matching machinery +SYSTEMD_LOG_LEVEL=debug journalctl -b -n 1 /dev/null /dev/zero /dev/null /dev/null /dev/null +journalctl -b -n 1 /bin/true /bin/false +journalctl -b -n 1 /bin/true + /bin/false +journalctl -b -n 1 -r --unit "systemd*" + +systemd-run --user -M "testuser@.host" /bin/echo hello +journalctl --sync +journalctl -b -n 1 -r --user-unit "*" + +(! journalctl -b /dev/lets-hope-this-doesnt-exist) +(! journalctl -b /dev/null /dev/zero /dev/this-also-shouldnt-exist) +(! journalctl -b --unit "this-unit-should-not-exist*") + +# Facilities & priorities +journalctl --facility help +journalctl --facility kern -n 1 +journalctl --facility syslog --priority 0..3 -n 1 +journalctl --facility syslog --priority 3..0 -n 1 +journalctl --facility user --priority 0..0 -n 1 +journalctl --facility daemon --priority warning -n 1 +journalctl --facility daemon --priority warning..info -n 1 +journalctl --facility daemon --priority notice..crit -n 1 +journalctl --facility daemon --priority 5..crit -n 1 + +# Assorted combinations +journalctl -o help +journalctl -q -n all -a | grep . >/dev/null +journalctl -q --no-full | grep . >/dev/null +journalctl -q --user --system | grep . >/dev/null +journalctl --namespace "*" | grep . >/dev/null +journalctl --namespace "" | grep . >/dev/null +journalctl -q --namespace "+foo-bar-baz-$RANDOM" | grep . >/dev/null +(! journalctl -q --namespace "foo-bar-baz-$RANDOM" | grep .) +journalctl --root / | grep . >/dev/null +journalctl --cursor "t=0;t=-1;t=0;t=0x0" | grep . >/dev/null +journalctl --header | grep system.journal +journalctl --field _EXE | grep . >/dev/null +journalctl --no-hostname --utc --catalog | grep . >/dev/null +# Exercise executable_is_script() and the related code, e.g. `journalctl -b /path/to/a/script.sh` should turn +# into ((_EXE=/bin/bash AND _COMM=script.sh) AND _BOOT_ID=c002e3683ba14fa8b6c1e12878386514) +journalctl -b "$(readlink -f "$0")" | grep . >/dev/null +journalctl -b "$(systemd-id128 boot-id)" | grep . >/dev/null +journalctl --since yesterday --reverse | grep . >/dev/null +journalctl --machine .host | grep . >/dev/null +# Log something that journald will forward to wall +echo "Oh no!" | systemd-cat -t "emerg$RANDOM" -p emerg --stderr-priority emerg + +TAG="$(systemd-id128 new)" +echo "Foo Bar Baz" | systemd-cat -t "$TAG" +journalctl --sync +# Relevant excerpt from journalctl(1): +# If the pattern is all lowercase, matching is case insensitive. Otherwise, matching is case sensitive. +# This can be overridden with the --case-sensitive option +journalctl -e -t "$TAG" --grep "Foo Bar Baz" +journalctl -e -t "$TAG" --grep "foo bar baz" +(! journalctl -e -t "$TAG" --grep "foo Bar baz") +journalctl -e -t "$TAG" --case-sensitive=false --grep "foo Bar baz" + +(! journalctl --facility hopefully-an-unknown-facility) +(! journalctl --priority hello-world) +(! journalctl --priority 0..128) +(! journalctl --priority 0..systemd) + +# Other options +journalctl --disk-usage +journalctl --dmesg -n 1 +journalctl --fields +journalctl --list-boots +journalctl --update-catalog +journalctl --list-catalog + +# Add new tests before here, the journald restarts below +# may make tests flappy. + +# Don't lose streams on restart +systemctl start forever-print-hola +sleep 3 +systemctl restart systemd-journald +sleep 3 +systemctl stop forever-print-hola +[[ ! -f "/tmp/i-lose-my-logs" ]] + +# https://github.com/systemd/systemd/issues/4408 +rm -f /tmp/i-lose-my-logs +systemctl start forever-print-hola +sleep 3 +systemctl kill --signal=SIGKILL systemd-journald +sleep 3 +[[ ! -f "/tmp/i-lose-my-logs" ]] +systemctl stop forever-print-hola + +set +o pipefail +# https://github.com/systemd/systemd/issues/15528 +journalctl --follow --file=/var/log/journal/*/* | head -n1 | grep . +# https://github.com/systemd/systemd/issues/24565 +journalctl --follow --merge | head -n1 | grep . +set -o pipefail + +# https://github.com/systemd/systemd/issues/26746 +rm -f /tmp/issue-26746-log /tmp/issue-26746-cursor +ID="$(systemd-id128 new)" +journalctl -t "$ID" --follow --cursor-file=/tmp/issue-26746-cursor | tee /tmp/issue-26746-log & +systemd-cat -t "$ID" /bin/sh -c 'echo hogehoge' +# shellcheck disable=SC2016 +timeout 10 bash -c 'until [[ -f /tmp/issue-26746-log && "$(cat /tmp/issue-26746-log)" =~ hogehoge ]]; do sleep .5; done' +pkill -TERM journalctl +timeout 10 bash -c 'until test -f /tmp/issue-26746-cursor; do sleep .5; done' +CURSOR_FROM_FILE="$(cat /tmp/issue-26746-cursor)" +CURSOR_FROM_JOURNAL="$(journalctl -t "$ID" --output=export MESSAGE=hogehoge | sed -n -e '/__CURSOR=/ { s/__CURSOR=//; p }')" +test "$CURSOR_FROM_FILE" = "$CURSOR_FROM_JOURNAL" + +# Check that the seqnum field at least superficially works +systemd-cat echo "ya" +journalctl --sync +SEQNUM1=$(journalctl -o export -n 1 | grep -Ea "^__SEQNUM=" | cut -d= -f2) +systemd-cat echo "yo" +journalctl --sync +SEQNUM2=$(journalctl -o export -n 1 | grep -Ea "^__SEQNUM=" | cut -d= -f2) +test "$SEQNUM2" -gt "$SEQNUM1" + +# Test for journals without RTC +# See: https://github.com/systemd/systemd/issues/662 +JOURNAL_DIR="$(mktemp -d)" +while read -r file; do + filename="${file##*/}" + unzstd "$file" -o "$JOURNAL_DIR/${filename%*.zst}" +done < <(find /test-journals/no-rtc -name "*.zst") + +journalctl --directory="$JOURNAL_DIR" --list-boots --output=json >/tmp/lb1 +diff -u /tmp/lb1 - <<'EOF' +[{"index":-3,"boot_id":"5ea5fc4f82a14186b5332a788ef9435e","first_entry":1666569600994371,"last_entry":1666584266223608},{"index":-2,"boot_id":"bea6864f21ad4c9594c04a99d89948b0","first_entry":1666569601005945,"last_entry":1666584347230411},{"index":-1,"boot_id":"4c708e1fd0744336be16f3931aa861fb","first_entry":1666569601017222,"last_entry":1666584354649355},{"index":0,"boot_id":"35e8501129134edd9df5267c49f744a4","first_entry":1666569601009823,"last_entry":1666584438086856}] +EOF +rm -rf "$JOURNAL_DIR" /tmp/lb1 + +# Check that using --after-cursor/--cursor-file= together with journal filters doesn't +# skip over entries matched by the filter +# See: https://github.com/systemd/systemd/issues/30288 +UNIT_NAME="test-cursor-$RANDOM.service" +CURSOR_FILE="$(mktemp)" +# Generate some messages we can match against +journalctl --cursor-file="$CURSOR_FILE" -n1 +systemd-run --unit="$UNIT_NAME" --wait --service-type=exec bash -xec "echo hello; echo world" +journalctl --sync +# --after-cursor= + --unit= +# The format of the "Starting ..." message depends on StatusUnitFormat=, so match only the beginning +# which should be enough in this case +[[ "$(journalctl -n 1 -p info -o cat --unit="$UNIT_NAME" --after-cursor="$(<"$CURSOR_FILE")" _PID=1 )" =~ ^Starting\ ]] +# There should be no such messages before the cursor +[[ -z "$(journalctl -n 1 -p info -o cat --unit="$UNIT_NAME" --after-cursor="$(<"$CURSOR_FILE")" --reverse)" ]] +# --cursor-file= + a journal filter +diff <(journalctl --cursor-file="$CURSOR_FILE" -p info -o cat _SYSTEMD_UNIT="$UNIT_NAME") - <<EOF ++ echo hello +hello ++ echo world +world +EOF +rm -f "$CURSOR_FILE" diff --git a/test/units/testsuite-04.service b/test/units/testsuite-04.service new file mode 100644 index 0000000..63a0104 --- /dev/null +++ b/test/units/testsuite-04.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-04-JOURNAL + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-04.sh b/test/units/testsuite-04.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-04.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-05.service b/test/units/testsuite-05.service new file mode 100644 index 0000000..ab72d8f --- /dev/null +++ b/test/units/testsuite-05.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-05-RLIMITS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-05.sh b/test/units/testsuite-05.sh new file mode 100755 index 0000000..870845d --- /dev/null +++ b/test/units/testsuite-05.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +P=/run/systemd/system.conf.d +mkdir $P + +cat >$P/rlimits.conf <<EOF +[Manager] +DefaultLimitNOFILE=10000:16384 +EOF + +systemctl daemon-reload + +[[ "$(systemctl show -P DefaultLimitNOFILESoft)" = "10000" ]] +[[ "$(systemctl show -P DefaultLimitNOFILE)" = "16384" ]] + +[[ "$(systemctl show -P LimitNOFILESoft testsuite-05.service)" = "10000" ]] +[[ "$(systemctl show -P LimitNOFILE testsuite-05.service)" = "16384" ]] + +# shellcheck disable=SC2016 +systemd-run --wait -t bash -c '[[ "$(ulimit -n -S)" = "10000" ]]' +# shellcheck disable=SC2016 +systemd-run --wait -t bash -c '[[ "$(ulimit -n -H)" = "16384" ]]' + +touch /testok diff --git a/test/units/testsuite-06.service b/test/units/testsuite-06.service new file mode 100644 index 0000000..c4c1d87 --- /dev/null +++ b/test/units/testsuite-06.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-06-SELINUX + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-06.sh b/test/units/testsuite-06.sh new file mode 100755 index 0000000..7fc3c37 --- /dev/null +++ b/test/units/testsuite-06.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Note: ATTOW the following checks should work with both Fedora and upstream reference policy +# (with or without MCS/MLS) + +sestatus + +# We should end up in permissive mode +[[ "$(getenforce)" == "Permissive" ]] + +# Check PID 1's context +PID1_CONTEXT="$(ps -h -o label 1)" +[[ "$PID1_CONTEXT" =~ ^system_u:system_r:init_t(:s0)?$ ]] +# The same label should be attached to all PID 1's journal messages +journalctl -q -b -p info -n 5 --grep . _SELINUX_CONTEXT="$PID1_CONTEXT" + +# Check context on a couple of arbitrarily-selected files/directories +[[ "$(stat --printf %C /run/systemd/journal/)" =~ ^system_u:object_r:(syslogd_runtime_t|syslogd_var_run_t)(:s0)?$ ]] +[[ "$(stat --printf %C /run/systemd/notify)" =~ ^system_u:object_r:(init_runtime_t|init_var_run_t)(:s0)?$ ]] +[[ "$(stat --printf %C /run/systemd/sessions/)" =~ ^system_u:object_r:(systemd_sessions_runtime_t|systemd_logind_sessions_t)(:s0)?$ ]] + +# Check if our SELinux-related functionality works +# +# Since the SELinux policies vary wildly, use a context from some existing file +# as our test context +CONTEXT="$(stat -c %C /proc/sys/kernel/core_pattern)" + +[[ "$(systemd-run --wait --pipe -p SELinuxContext="$CONTEXT" cat /proc/self/attr/current | tr -d '\0')" == "$CONTEXT" ]] +(! systemd-run --wait --pipe -p SELinuxContext="foo:bar:baz" cat /proc/self/attr/current) +(! systemd-run --wait --pipe -p ConditionSecurity='selinux' false) +systemd-run --wait --pipe -p ConditionSecurity='!selinux' false + +NSPAWN_ARGS=(systemd-nspawn -q --volatile=yes --directory=/ --bind-ro=/etc --inaccessible=/etc/machine-id) +[[ "$("${NSPAWN_ARGS[@]}" cat /proc/self/attr/current | tr -d '\0')" != "$CONTEXT" ]] +[[ "$("${NSPAWN_ARGS[@]}" --selinux-context="$CONTEXT" cat /proc/self/attr/current | tr -d '\0')" == "$CONTEXT" ]] +[[ "$("${NSPAWN_ARGS[@]}" stat --printf %C /run)" != "$CONTEXT" ]] +[[ "$("${NSPAWN_ARGS[@]}" --selinux-apifs-context="$CONTEXT" stat --printf %C /run)" == "$CONTEXT" ]] +[[ "$("${NSPAWN_ARGS[@]}" --selinux-apifs-context="$CONTEXT" --tmpfs=/tmp stat --printf %C /tmp)" == "$CONTEXT" ]] + +touch /testok diff --git a/test/units/testsuite-07.exec-context.sh b/test/units/testsuite-07.exec-context.sh new file mode 100755 index 0000000..66e8fce --- /dev/null +++ b/test/units/testsuite-07.exec-context.sh @@ -0,0 +1,375 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Make sure the unit's exec context matches its configuration +# See: https://github.com/systemd/systemd/pull/29552 + +# Even though hidepid= was introduced in kernel 3.3, we support only +# the post 5.8 implementation that allows us to apply the option per-instance, +# instead of the whole namespace. To distinguish between these two implementations +# lets check if we can mount procfs with a named value (e.g. hidepid=off), since +# support for this was introduced in the same commit as the per-instance stuff +proc_supports_option() { + local option="${1:?}" + local proc_tmp ec + + proc_tmp="$(mktemp -d)" + mount -t proc -o "$option" proc "$proc_tmp" && ec=0 || ec=$? + mountpoint -q "$proc_tmp" && umount -q "$proc_tmp" + rm -rf "$proc_tmp" + + return $ec +} + +# In coverage builds we disable ProtectSystem= and ProtectHome= via a service.d +# dropin in /etc. This dropin has, unfortunately, higher priority than +# the transient stuff from systemd-run. Let's just skip the following tests +# in that case instead of complicating the test setup even more */ +if [[ -z "${COVERAGE_BUILD_DIR:-}" ]]; then + systemd-run --wait --pipe -p ProtectSystem=yes \ + bash -xec "test ! -w /usr; test ! -w /boot; test -w /etc; test -w /var" + systemd-run --wait --pipe -p ProtectSystem=full \ + bash -xec "test ! -w /usr; test ! -w /boot; test ! -w /etc; test -w /var" + systemd-run --wait --pipe -p ProtectSystem=strict \ + bash -xec "test ! -w /; test ! -w /etc; test ! -w /var; test -w /dev; test -w /proc" + systemd-run --wait --pipe -p ProtectSystem=no \ + bash -xec "test -w /; test -w /etc; test -w /var; test -w /dev; test -w /proc" + + MARK="$(mktemp /root/.exec-context.XXX)" + systemd-run --wait --pipe -p ProtectHome=yes \ + bash -xec "test ! -w /home; test ! -w /root; test ! -w /run/user; test ! -e $MARK" + systemd-run --wait --pipe -p ProtectHome=read-only \ + bash -xec "test ! -w /home; test ! -w /root; test ! -w /run/user; test -e $MARK" + systemd-run --wait --pipe -p ProtectHome=tmpfs \ + bash -xec "test -w /home; test -w /root; test -w /run/user; test ! -e $MARK" + systemd-run --wait --pipe -p ProtectHome=no \ + bash -xec "test -w /home; test -w /root; test -w /run/user; test -e $MARK" + rm -f "$MARK" +fi + +if proc_supports_option "hidepid=off"; then + systemd-run --wait --pipe -p ProtectProc=noaccess -p User=testuser \ + bash -xec 'test -e /proc/1; test ! -r /proc/1; test -r /proc/$$$$/comm' + systemd-run --wait --pipe -p ProtectProc=invisible -p User=testuser \ + bash -xec 'test ! -e /proc/1; test -r /proc/$$$$/comm' + systemd-run --wait --pipe -p ProtectProc=ptraceable -p User=testuser \ + bash -xec 'test ! -e /proc/1; test -r /proc/$$$$/comm' + systemd-run --wait --pipe -p ProtectProc=ptraceable -p User=testuser -p AmbientCapabilities=CAP_SYS_PTRACE \ + bash -xec 'test -r /proc/1; test -r /proc/$$$$/comm' + systemd-run --wait --pipe -p ProtectProc=default -p User=testuser \ + bash -xec 'test -r /proc/1; test -r /proc/$$$$/comm' +fi + +if proc_supports_option "subset=pid"; then + systemd-run --wait --pipe -p ProcSubset=pid -p User=testuser \ + bash -xec "test -r /proc/1/comm; test ! -e /proc/cpuinfo" + systemd-run --wait --pipe -p ProcSubset=all -p User=testuser \ + bash -xec "test -r /proc/1/comm; test -r /proc/cpuinfo" +fi + +if ! systemd-detect-virt -cq; then + systemd-run --wait --pipe -p ProtectKernelLogs=yes -p User=testuser \ + bash -xec "test ! -r /dev/kmsg" + systemd-run --wait --pipe -p ProtectKernelLogs=no -p User=testuser \ + bash -xec "test -r /dev/kmsg" +fi + +systemd-run --wait --pipe -p BindPaths="/etc /home:/mnt:norbind -/foo/bar/baz:/usr:rbind" \ + bash -xec "mountpoint /etc; test -d /etc/systemd; mountpoint /mnt; ! mountpoint /usr" +systemd-run --wait --pipe -p BindReadOnlyPaths="/etc /home:/mnt:norbind -/foo/bar/baz:/usr:rbind" \ + bash -xec "test ! -w /etc; test ! -w /mnt; ! mountpoint /usr" +# Make sure we properly serialize/deserialize paths with spaces +# See: https://github.com/systemd/systemd/issues/30747 +touch "/tmp/test file with spaces" +systemd-run --wait --pipe -p TemporaryFileSystem="/tmp" -p BindPaths="/etc /home:/mnt:norbind /tmp/test\ file\ with\ spaces" \ + bash -xec "mountpoint /etc; test -d /etc/systemd; mountpoint /mnt; stat '/tmp/test file with spaces'" +systemd-run --wait --pipe -p TemporaryFileSystem="/tmp" -p BindPaths="/etc /home:/mnt:norbind /tmp/test\ file\ with\ spaces:/tmp/destination\ wi\:th\ spaces" \ + bash -xec "mountpoint /etc; test -d /etc/systemd; mountpoint /mnt; stat '/tmp/destination wi:th spaces'" + +# Check if we correctly serialize, deserialize, and set directives that +# have more complex internal handling +if ! systemd-detect-virt -cq; then + # Funny detail: this originally used the underlying rootfs device, but that, + # for some reason, caused "divide error" in kernel, followed by a kernel panic + TEMPFILE="$(mktemp)" + LODEV="$(losetup --show -f "$TEMPFILE")" + ROOT_DEV_MAJ_MIN="$(lsblk -nro MAJ:MIN "$LODEV")" + EXPECTED_IO_MAX="$ROOT_DEV_MAJ_MIN rbps=1000 wbps=1000000000000 riops=2000000000 wiops=4000" + EXPECTED_IO_LATENCY="$ROOT_DEV_MAJ_MIN target=69000" + SERVICE_NAME="test-io-directives-$RANDOM.service" + CGROUP_PATH="/sys/fs/cgroup/system.slice/$SERVICE_NAME" + + # IO*= + ARGUMENTS=( + # Throw in a couple of invalid entries just to test things out + -p IOReadBandwidthMax="/foo/bar 1M" + -p IOReadBandwidthMax="/foo/baz 1M" + -p IOReadBandwidthMax="$LODEV 1M" + -p IOReadBandwidthMax="$LODEV 1K" + -p IOWriteBandwidthMax="$LODEV 1G" + -p IOWriteBandwidthMax="$LODEV 1T" + -p IOReadIOPSMax="$LODEV 2G" + -p IOWriteIOPSMax="$LODEV 4K" + -p IODeviceLatencyTargetSec="$LODEV 666ms" + -p IODeviceLatencyTargetSec="/foo/bar 69ms" + -p IODeviceLatencyTargetSec="$LODEV 69ms" + -p IOReadBandwidthMax="/foo/bar 1M" + -p IOReadBandwidthMax="/foo/baz 1M" + # TODO: IODeviceWeight= doesn't work on loop devices and virtual disks + -p IODeviceWeight="$LODEV 999" + -p IODeviceWeight="/foo/bar 999" + ) + + systemctl set-property system.slice IOAccounting=yes + # io.latency not available by default on Debian stable + if [[ -e /sys/fs/cgroup/system.slice/io.latency ]]; then + systemd-run --wait --pipe --unit "$SERVICE_NAME" "${ARGUMENTS[@]}" \ + bash -xec "diff <(echo $EXPECTED_IO_MAX) $CGROUP_PATH/io.max; diff <(echo $EXPECTED_IO_LATENCY) $CGROUP_PATH/io.latency" + fi + + # CPUScheduling= + ARGUMENTS=( + -p CPUSchedulingPolicy=rr # ID: 2 + -p CPUSchedulingPolicy=fifo # ID: 1 + -p CPUSchedulingPriority=5 # Actual prio: 94 (99 - prio) + -p CPUSchedulingPriority=10 # Actual prio: 89 (99 - prio) + ) + + systemd-run --wait --pipe --unit "$SERVICE_NAME" "${ARGUMENTS[@]}" \ + bash -xec 'grep -E "^policy\s*:\s*1$" /proc/self/sched; grep -E "^prio\s*:\s*89$" /proc/self/sched' + + # Device*= + ARGUMENTS=( + -p DevicePolicy=closed + -p DevicePolicy=strict + -p DeviceAllow="char-mem rm" # Allow read & mknod for /dev/{null,zero,...} + -p DeviceAllow="/dev/loop0 rw" + -p DeviceAllow="/dev/loop0 w" # Allow write for /dev/loop0 + # Everything else should be disallowed per the strict policy + ) + + systemd-run --wait --pipe --unit "$SERVICE_NAME" "${ARGUMENTS[@]}" \ + bash -xec 'test -r /dev/null; test ! -w /dev/null; test ! -r /dev/loop0; test -w /dev/loop0; test ! -r /dev/tty; test ! -w /dev/tty' + + if ! systemctl --version | grep -qF -- "-BPF_FRAMEWORK"; then + # SocketBind*= + ARGUMENTS=( + -p SocketBindAllow= + -p SocketBindAllow=1234 + -p SocketBindAllow=ipv4:udp:any + -p SocketBindAllow=ipv6:6666 + # Everything but the last assignment is superfluous, but it still exercises + # the parsing machinery + -p SocketBindDeny= + -p SocketBindDeny=1111 + -p SocketBindDeny=ipv4:1111 + -p SocketBindDeny=ipv4:any + -p SocketBindDeny=ipv4:tcp:any + -p SocketBindDeny=ipv4:udp:10000-11000 + -p SocketBindDeny=ipv6:1111 + -p SocketBindDeny=any + ) + + # We should fail with EPERM when trying to bind to a socket not on the allow list + # (nc exits with 2 in that case) + systemd-run --wait -p SuccessExitStatus="1 2" --pipe "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -l 127.0.0.1 9999; exit 42' + systemd-run --wait -p SuccessExitStatus="1 2" --pipe "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -l ::1 9999; exit 42' + systemd-run --wait -p SuccessExitStatus="1 2" --pipe "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -6 -u -l ::1 9999; exit 42' + systemd-run --wait -p SuccessExitStatus="1 2" --pipe "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -4 -l 127.0.0.1 6666; exit 42' + # Consequently, we should succeed when binding to a socket on the allow list + # and keep listening on it until we're killed by `timeout` (EC 124) + systemd-run --wait --pipe -p SuccessExitStatus=124 "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -4 -l 127.0.0.1 1234; exit 1' + systemd-run --wait --pipe -p SuccessExitStatus=124 "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -4 -u -l 127.0.0.1 5678; exit 1' + systemd-run --wait --pipe -p SuccessExitStatus=124 "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -6 -l ::1 1234; exit 1' + systemd-run --wait --pipe -p SuccessExitStatus=124 "${ARGUMENTS[@]}" \ + bash -xec 'timeout 1s nc -6 -l ::1 6666; exit 1' + fi + + losetup -d "$LODEV" + rm -f "$TEMPFILE" +fi + +# {Cache,Configuration,Logs,Runtime,State}Directory= +ARGUMENTS=( + -p CacheDirectory="foo/bar/baz also\ with\ spaces" + -p CacheDirectory="foo" + -p CacheDirectory="context" + -p CacheDirectoryMode="0123" + -p CacheDirectoryMode="0666" + -p ConfigurationDirectory="context/foo also_context/bar context/nested/baz context/semi\:colon" + -p ConfigurationDirectoryMode="0400" + -p LogsDirectory="context/foo" + -p LogsDirectory="" + -p LogsDirectory="context/a/very/nested/logs/dir" + -p RuntimeDirectory="context/with\ spaces" + # Note: {Runtime,State,Cache,Logs}Directory= directives support the directory:symlink syntax, which + # requires an additional level of escaping for the colon character + -p RuntimeDirectory="also_context:a\ symlink\ with\ \\\:\ col\\\:ons\ and\ \ spaces" + -p RuntimeDirectoryPreserve=yes + -p StateDirectory="context" + -p StateDirectory="./././././././context context context" + -p StateDirectoryMode="0000" +) + +rm -rf /run/context +systemd-run --wait --pipe "${ARGUMENTS[@]}" \ + bash -xec '[[ $CACHE_DIRECTORY == "/var/cache/also with spaces:/var/cache/context:/var/cache/foo:/var/cache/foo/bar/baz" ]]; + [[ $(stat -c "%a" "${CACHE_DIRECTORY##*:}") == 666 ]]' +systemd-run --wait --pipe "${ARGUMENTS[@]}" \ + bash -xec '[[ $CONFIGURATION_DIRECTORY == /etc/also_context/bar:/etc/context/foo:/etc/context/nested/baz:/etc/context/semi:colon ]]; + [[ $(stat -c "%a" "${CONFIGURATION_DIRECTORY%%:*}") == 400 ]]' +systemd-run --wait --pipe "${ARGUMENTS[@]}" \ + bash -xec '[[ $LOGS_DIRECTORY == /var/log/context/a/very/nested/logs/dir:/var/log/context/foo ]]; + [[ $(stat -c "%a" "${LOGS_DIRECTORY##*:}") == 755 ]]' +systemd-run --wait --pipe "${ARGUMENTS[@]}" \ + bash -xec '[[ $RUNTIME_DIRECTORY == "/run/also_context:/run/context/with spaces" ]]; + [[ $(stat -c "%a" "${RUNTIME_DIRECTORY##*:}") == 755 ]]; + [[ $(stat -c "%a" "${RUNTIME_DIRECTORY%%:*}") == 755 ]]' +systemd-run --wait --pipe "${ARGUMENTS[@]}" \ + bash -xec '[[ $STATE_DIRECTORY == /var/lib/context ]]; [[ $(stat -c "%a" $STATE_DIRECTORY) == 0 ]]' +test -d "/run/context/with spaces" +test -s "/run/a symlink with : col:ons and spaces" +rm -rf /var/{cache,lib,log}/context /etc/{also_,}context + +# Limit*= +# +# Note: keep limits of LimitDATA= and LimitAS= unlimited, otherwise ASan (LSan) +# won't be able to mmap the shadow maps +ARGUMENTS=( + -p LimitCPU=15 + -p LimitCPU=10:15 # ulimit -t + -p LimitFSIZE=96G # ulimit -f + -p LimitDATA=8T:infinity + -p LimitDATA=infinity # ulimit -d + -p LimitSTACK=8M # ulimit -s + -p LimitCORE=infinity + -p LimitCORE=17M # ulimit -c + -p LimitRSS=27G # ulimit -m + -p LimitNOFILE=7:127 # ulimit -n + -p LimitAS=infinity # ulimit -v + -p LimitNPROC=1 + -p LimitNPROC=64:infinity # ulimit -u + -p LimitMEMLOCK=37M # ulimit -l + -p LimitLOCKS=19:1021 # ulimit -x + -p LimitSIGPENDING=21 # ulimit -i + -p LimitMSGQUEUE=666 # ulimit -q + -p LimitNICE=4 # ulimit -e + -p LimitRTPRIO=8 # ulimit -r + -p LimitRTTIME=666666 # ulimit -R +) +# Do all the checks in one giant inline shell blob to avoid the overhead of spawning +# a new service for each check +# +# Note: ulimit shows storage-related values in 1024-byte increments* +# Note2: ulimit -R requires bash >= 5.1 +# +# * in POSIX mode -c a -f options show values in 512-byte increments; let's hope +# we never run in the POSIX mode +systemd-run --wait --pipe "${ARGUMENTS[@]}" \ + bash -xec 'KB=1; MB=$((KB * 1024)); GB=$((MB * 1024)); TB=$((GB * 1024)); + : CPU; [[ $(ulimit -St) -eq 10 ]]; [[ $(ulimit -Ht) -eq 15 ]]; + : FSIZE; [[ $(ulimit -Sf) -eq $((96 * GB)) ]]; [[ $(ulimit -Hf) -eq $((96 * GB)) ]]; + : DATA; [[ $(ulimit -Sd) == unlimited ]]; [[ $(ulimit -Hd) == unlimited ]]; + : STACK; [[ $(ulimit -Ss) -eq $((8 * MB)) ]]; [[ $(ulimit -Hs) -eq $((8 * MB)) ]]; + : CORE; [[ $(ulimit -Sc) -eq $((17 * MB)) ]]; [[ $(ulimit -Hc) -eq $((17 * MB)) ]]; + : RSS; [[ $(ulimit -Sm) -eq $((27 * GB)) ]]; [[ $(ulimit -Hm) -eq $((27 * GB)) ]]; + : NOFILE; [[ $(ulimit -Sn) -eq 7 ]]; [[ $(ulimit -Hn) -eq 127 ]]; + : AS; [[ $(ulimit -Sv) == unlimited ]]; [[ $(ulimit -Hv) == unlimited ]]; + : NPROC; [[ $(ulimit -Su) -eq 64 ]]; [[ $(ulimit -Hu) == unlimited ]]; + : MEMLOCK; [[ $(ulimit -Sl) -eq $((37 * MB)) ]]; [[ $(ulimit -Hl) -eq $((37 * MB)) ]]; + : LOCKS; [[ $(ulimit -Sx) -eq 19 ]]; [[ $(ulimit -Hx) -eq 1021 ]]; + : SIGPENDING; [[ $(ulimit -Si) -eq 21 ]]; [[ $(ulimit -Hi) -eq 21 ]]; + : MSGQUEUE; [[ $(ulimit -Sq) -eq 666 ]]; [[ $(ulimit -Hq) -eq 666 ]]; + : NICE; [[ $(ulimit -Se) -eq 4 ]]; [[ $(ulimit -He) -eq 4 ]]; + : RTPRIO; [[ $(ulimit -Sr) -eq 8 ]]; [[ $(ulimit -Hr) -eq 8 ]]; + ulimit -R || exit 0; + : RTTIME; [[ $(ulimit -SR) -eq 666666 ]]; [[ $(ulimit -HR) -eq 666666 ]];' + +# RestrictFileSystems= +# +# Note: running instrumented binaries requires at least /proc to be accessible, so let's +# skip the test when we're running under sanitizers +# +# Note: $GCOV_ERROR_LOG is used during coverage runs to suppress errors when creating *.gcda files, +# since gcov can't access the restricted filesystem (as expected) +if [[ ! -v ASAN_OPTIONS ]] && systemctl --version | grep "+BPF_FRAMEWORK" && kernel_supports_lsm bpf; then + ROOTFS="$(df --output=fstype /usr/bin | sed --quiet 2p)" + systemd-run --wait --pipe -p RestrictFileSystems="" ls / + systemd-run --wait --pipe -p RestrictFileSystems="$ROOTFS foo bar" ls / + (! systemd-run --wait --pipe -p RestrictFileSystems="$ROOTFS" ls /proc) + (! systemd-run --wait --pipe -p GCOV_ERROR_LOG=/dev/null -p RestrictFileSystems="foo" ls /) + systemd-run --wait --pipe -p RestrictFileSystems="$ROOTFS foo bar baz proc" ls /proc + systemd-run --wait --pipe -p RestrictFileSystems="$ROOTFS @foo @basic-api" ls /proc + systemd-run --wait --pipe -p RestrictFileSystems="$ROOTFS @foo @basic-api" ls /sys/fs/cgroup + + systemd-run --wait --pipe -p RestrictFileSystems="~" ls / + systemd-run --wait --pipe -p RestrictFileSystems="~proc" ls / + systemd-run --wait --pipe -p RestrictFileSystems="~@basic-api" ls / + (! systemd-run --wait --pipe -p GCOV_ERROR_LOG=/dev/null -p RestrictFileSystems="~$ROOTFS" ls /) + (! systemd-run --wait --pipe -p RestrictFileSystems="~proc" ls /proc) + (! systemd-run --wait --pipe -p RestrictFileSystems="~@basic-api" ls /proc) + (! systemd-run --wait --pipe -p RestrictFileSystems="~proc foo @bar @basic-api" ls /proc) + (! systemd-run --wait --pipe -p RestrictFileSystems="~proc foo @bar @basic-api" ls /sys) + systemd-run --wait --pipe -p RestrictFileSystems="~proc devtmpfs sysfs" ls / + (! systemd-run --wait --pipe -p RestrictFileSystems="~proc devtmpfs sysfs" ls /proc) + (! systemd-run --wait --pipe -p RestrictFileSystems="~proc devtmpfs sysfs" ls /dev) + (! systemd-run --wait --pipe -p RestrictFileSystems="~proc devtmpfs sysfs" ls /sys) +fi + +# Make sure we properly (de)serialize various string arrays, including whitespaces +# See: https://github.com/systemd/systemd/issues/31214 +systemd-run --wait --pipe -p Environment="FOO='bar4 '" \ + bash -xec '[[ $FOO == "bar4 " ]]' +systemd-run --wait --pipe -p Environment="FOO='bar4 ' BAR='\n\n'" \ + bash -xec "[[ \$FOO == 'bar4 ' && \$BAR == $'\n\n' ]]" +systemd-run --wait --pipe -p Environment='FOO="bar4 \\ "' -p Environment="BAR='\n\t'" \ + bash -xec "[[ \$FOO == 'bar4 \\ ' && \$BAR == $'\n\t' ]]" +TEST_ENV_FILE="/tmp/test-env-file-$RANDOM- " +cat >"$TEST_ENV_FILE" <<EOF +FOO="env file " +BAR=" + " +EOF +systemd-run --wait --pipe cat "$TEST_ENV_FILE" +systemd-run --wait --pipe -p ReadOnlyPaths="'$TEST_ENV_FILE'" \ + bash -xec '[[ ! -w "$TEST_ENV_FILE" ]]' +systemd-run --wait --pipe -p PrivateTmp=yes -p BindReadOnlyPaths="'$TEST_ENV_FILE':'/tmp/bar- '" \ + bash -xec '[[ -e "/tmp/bar- " && ! -w "/tmp/bar- " ]]' +systemd-run --wait --pipe -p EnvironmentFile="$TEST_ENV_FILE" \ + bash -xec "[[ \$FOO == 'env file ' && \$BAR == $'\n ' ]]" +rm -f "$TEST_ENV_FILE" +# manager_serialize()/manager_deserialize() uses similar machinery +systemctl unset-environment FOO_WITH_SPACES +systemctl set-environment FOO_WITH_SPACES="foo " FOO_WITH_TABS="foo\t\t\t" +systemctl show-environment +systemctl show-environment | grep -F "FOO_WITH_SPACES=$'foo '" +systemctl show-environment | grep -F "FOO_WITH_TABS=$'foo\\\\t\\\\t\\\\t'" +systemctl daemon-reexec +systemctl show-environment +systemctl show-environment | grep -F "FOO_WITH_SPACES=$'foo '" +systemctl show-environment | grep -F "FOO_WITH_TABS=$'foo\\\\t\\\\t\\\\t'" + +# Ensure that clean-up codepaths work correctly if activation ultimately fails +touch /run/not-a-directory +mkdir /tmp/root +touch /tmp/root/foo +chmod +x /tmp/root/foo +(! systemd-run --wait --pipe false) +(! systemd-run --wait --pipe --unit "test-dynamicuser-fail" -p DynamicUser=yes -p WorkingDirectory=/nonexistent true) +(! systemd-run --wait --pipe -p RuntimeDirectory=not-a-directory true) +(! systemd-run --wait --pipe -p RootDirectory=/tmp/root this-shouldnt-exist) +(! systemd-run --wait --pipe -p RootDirectory=/tmp/root /foo) +(! systemd-run --wait --pipe --service-type=oneshot -p ExecStartPre=-/foo/bar/baz -p ExecStart=-/foo/bar/baz -p RootDirectory=/tmp/root -- "- foo") diff --git a/test/units/testsuite-07.issue-14566.sh b/test/units/testsuite-07.issue-14566.sh new file mode 100755 index 0000000..d4be5b5 --- /dev/null +++ b/test/units/testsuite-07.issue-14566.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test that KillMode=mixed does not leave left over processes with ExecStopPost= +# Issue: https://github.com/systemd/systemd/issues/14566 + +if [[ -n "${ASAN_OPTIONS:-}" ]]; then + # Temporarily skip this test when running with sanitizers due to a deadlock + # See: https://bugzilla.redhat.com/show_bug.cgi?id=2098125 + echo "Sanitizers detected, skipping the test..." + exit 0 +fi + +systemctl start issue14566-repro +sleep 4 +systemctl status issue14566-repro + +leaked_pid=$(cat /leakedtestpid) + +systemctl stop issue14566-repro +sleep 4 + +# Leaked PID will still be around if we're buggy. +# I personally prefer to see 42. +ps -p "$leaked_pid" && exit 42 + +exit 0 diff --git a/test/units/testsuite-07.issue-16115.sh b/test/units/testsuite-07.issue-16115.sh new file mode 100755 index 0000000..8f63826 --- /dev/null +++ b/test/units/testsuite-07.issue-16115.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test ExecCondition= does not restart on abnormal or failure +# Issue: https://github.com/systemd/systemd/issues/16115 + +systemctl start issue16115-repro-1 +systemctl start issue16115-repro-2 +systemctl start issue16115-repro-3 +sleep 5 # wait a bit in case there are restarts so we can count them below + +[[ "$(systemctl show issue16115-repro-1 -P NRestarts)" == "0" ]] +[[ "$(systemctl show issue16115-repro-2 -P NRestarts)" == "0" ]] +[[ "$(systemctl show issue16115-repro-3 -P NRestarts)" == "0" ]] diff --git a/test/units/testsuite-07.issue-1981.sh b/test/units/testsuite-07.issue-1981.sh new file mode 100755 index 0000000..6eb802c --- /dev/null +++ b/test/units/testsuite-07.issue-1981.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Segmentation fault in timer_enter_waiting while masking a unit +# Issue: https://github.com/systemd/systemd/issues/1981 + +at_exit() { + set +e + + systemctl stop my.timer my.service + rm -f /run/systemd/system/my.{service,timer} + systemctl daemon-reload +} + +trap at_exit EXIT + +mkdir -p /run/systemd/system + +cat >/run/systemd/system/my.service <<\EOF +[Service] +Type=oneshot +ExecStartPre=sh -c 'test "$TRIGGER_UNIT" = my.timer' +ExecStartPre=sh -c 'test -n "$TRIGGER_TIMER_REALTIME_USEC"' +ExecStartPre=sh -c 'test -n "$TRIGGER_TIMER_MONOTONIC_USEC"' +ExecStart=/bin/echo Timer runs me +EOF + +cat >/run/systemd/system/my.timer <<EOF +[Timer] +OnBootSec=10s +OnUnitInactiveSec=1h +EOF + +systemctl unmask my.timer +systemctl start my.timer + +mkdir -p /run/systemd/system/my.timer.d/ +cat >/run/systemd/system/my.timer.d/override.conf <<EOF +[Timer] +OnBootSec=10s +OnUnitInactiveSec=1h +EOF + +systemctl daemon-reload +systemctl mask my.timer diff --git a/test/units/testsuite-07.issue-2467.sh b/test/units/testsuite-07.issue-2467.sh new file mode 100755 index 0000000..de0577b --- /dev/null +++ b/test/units/testsuite-07.issue-2467.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Don't start services every few ms if condition fails +# Issue: https://github.com/systemd/systemd/issues/2467 + +rm -f /tmp/nonexistent +systemctl start issue2467.socket +nc -i20 -w20 -U /run/test.ctl || : + +# TriggerLimitIntervalSec= by default is set to 2s. A "sleep 10" should give +# systemd enough time even on slower machines, to reach the trigger limit. +# shellcheck disable=SC2016 +timeout 10 bash -c 'until [[ "$(systemctl show issue2467.socket -P ActiveState)" == failed ]]; do sleep .5; done' +[[ "$(systemctl show issue2467.socket -P Result)" == trigger-limit-hit ]] diff --git a/test/units/testsuite-07.issue-27953.sh b/test/units/testsuite-07.issue-27953.sh new file mode 100755 index 0000000..8659970 --- /dev/null +++ b/test/units/testsuite-07.issue-27953.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Check if the unit doesn't remain in active state after the main PID exits +# Issue: https://github.com/systemd/systemd/issues/27953 + +systemctl start issue27953.service +timeout 10 sh -c 'while systemctl is-active issue27953.service; do sleep .5; done' +[[ "$(systemctl show -P ExitType issue27953.service)" == main ]] diff --git a/test/units/testsuite-07.issue-30412.sh b/test/units/testsuite-07.issue-30412.sh new file mode 100755 index 0000000..c1cb00e --- /dev/null +++ b/test/units/testsuite-07.issue-30412.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Check that socket FDs are not double closed on error: https://github.com/systemd/systemd/issues/30412 + +mkdir -p /run/systemd/system + +rm -f /tmp/badbin +touch /tmp/badbin +chmod 744 /tmp/badbin + +cat >/run/systemd/system/badbin_assert.service <<EOF +[Service] +ExecStart=/tmp/badbin +Restart=no +EOF + +cat >/run/systemd/system/badbin_assert.socket <<EOF +[Socket] +ListenStream=@badbin_assert.socket +FlushPending=yes +EOF + +systemctl daemon-reload +systemctl start badbin_assert.socket + +socat - ABSTRACT-CONNECT:badbin_assert.socket + +timeout 10 sh -c 'while systemctl is-active badbin_assert.service; do sleep .5; done' +[[ "$(systemctl show -P ExecMainStatus badbin_assert.service)" == 203 ]] diff --git a/test/units/testsuite-07.issue-3166.sh b/test/units/testsuite-07.issue-3166.sh new file mode 100755 index 0000000..6677901 --- /dev/null +++ b/test/units/testsuite-07.issue-3166.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Service doesn't enter the "failed" state +# Issue: https://github.com/systemd/systemd/issues/3166 + +systemctl --no-block start issue3166-fail-on-restart.service +active_state="$(systemctl show --value --property ActiveState issue3166-fail-on-restart.service)" +while [[ "$active_state" == "activating" || "$active_state" =~ ^(in)?active$ ]]; do + sleep .5 + active_state="$(systemctl show --value --property ActiveState issue3166-fail-on-restart.service)" +done +systemctl is-failed issue3166-fail-on-restart.service || exit 1 +[[ "$(systemctl show --value --property NRestarts issue3166-fail-on-restart.service)" -le 3 ]] || exit 1 diff --git a/test/units/testsuite-07.issue-3171.sh b/test/units/testsuite-07.issue-3171.sh new file mode 100755 index 0000000..db17c25 --- /dev/null +++ b/test/units/testsuite-07.issue-3171.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# SocketGroup lost on daemon-reload with unit moving away temporarily +# Issue: https://github.com/systemd/systemd/issues/3171 + +echo "g adm - - -" | systemd-sysusers - + +U=/run/systemd/system/issue-3171.socket +cat >$U <<EOF +[Unit] +Description=Test 12 socket +[Socket] +Accept=yes +ListenStream=/run/issue-3171.socket +SocketGroup=adm +SocketMode=0660 +EOF + +cat >/run/systemd/system/issue-3171@.service <<EOF +[Unit] +Description=Test service +[Service] +StandardInput=socket +ExecStart=/bin/sh -x -c cat +EOF + +systemctl start issue-3171.socket +systemctl is-active issue-3171.socket +[[ "$(stat --format='%G' /run/issue-3171.socket)" == adm ]] +echo A | nc -w1 -U /run/issue-3171.socket + +mv $U ${U}.disabled +systemctl daemon-reload +systemctl is-active issue-3171.socket +[[ "$(stat --format='%G' /run/issue-3171.socket)" == adm ]] +echo B | nc -w1 -U /run/issue-3171.socket && exit 1 + +mv ${U}.disabled $U +systemctl daemon-reload +systemctl is-active issue-3171.socket +echo C | nc -w1 -U /run/issue-3171.socket && exit 1 +[[ "$(stat --format='%G' /run/issue-3171.socket)" == adm ]] + +systemctl restart issue-3171.socket +systemctl is-active issue-3171.socket +echo D | nc -w1 -U /run/issue-3171.socket +[[ "$(stat --format='%G' /run/issue-3171.socket)" == adm ]] diff --git a/test/units/testsuite-07.main-PID-change.sh b/test/units/testsuite-07.main-PID-change.sh new file mode 100755 index 0000000..bd1144c --- /dev/null +++ b/test/units/testsuite-07.main-PID-change.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test changing the main PID + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# The main service PID should be the parent bash process +MAINPID="${PPID:?}" +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +# Start a test process inside of our own cgroup +sleep infinity & +INTERNALPID=$! +disown + +# Start a test process outside of our own cgroup +systemd-run -p DynamicUser=1 --unit=test-sleep.service /bin/sleep infinity +EXTERNALPID="$(systemctl show -P MainPID test-sleep.service)" + +# Update our own main PID to the external test PID, this should work +systemd-notify MAINPID="$EXTERNALPID" +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$EXTERNALPID" + +# Update our own main PID to the internal test PID, this should work, too +systemd-notify MAINPID=$INTERNALPID +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$INTERNALPID" + +# Update it back to our own PID, this should also work +systemd-notify MAINPID="$MAINPID" +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +# Try to set it to PID 1, which it should ignore, because that's the manager +systemd-notify MAINPID=1 +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +# Try to set it to PID 0, which is invalid and should be ignored +systemd-notify MAINPID=0 +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +# Try to set it to a valid but non-existing PID, which should be ignored. (Note +# that we set the PID to a value well above any known /proc/sys/kernel/pid_max, +# which means we can be pretty sure it doesn't exist by coincidence) +systemd-notify MAINPID=1073741824 +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +# Change it again to the external PID, without privileges this time. This should be ignored, because the PID is from outside of our cgroup and we lack privileges. +systemd-notify --uid=1000 MAINPID="$EXTERNALPID" +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +# Change it again to the internal PID, without privileges this time. This should work, as the process is on our cgroup, and that's enough even if we lack privileges. +systemd-notify --uid=1000 MAINPID="$INTERNALPID" +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$INTERNALPID" + +# Update it back to our own PID, this should also work +systemd-notify --uid=1000 MAINPID="$MAINPID" +test "$(systemctl show -P MainPID testsuite-07.service)" -eq "$MAINPID" + +cat >/tmp/test-mainpid.sh <<\EOF +#!/usr/bin/env bash + +set -eux +set -o pipefail + +# Create a number of children, and make one the main one +sleep infinity & +disown + +sleep infinity & +MAINPID=$! +disown + +sleep infinity & +disown + +echo $MAINPID >/run/mainpidsh/pid +EOF +chmod +x /tmp/test-mainpid.sh + +systemd-run --unit=test-mainpidsh.service \ + -p StandardOutput=tty \ + -p StandardError=tty \ + -p Type=forking \ + -p RuntimeDirectory=mainpidsh \ + -p PIDFile=/run/mainpidsh/pid \ + /tmp/test-mainpid.sh +test "$(systemctl show -P MainPID test-mainpidsh.service)" -eq "$(cat /run/mainpidsh/pid)" + +cat >/tmp/test-mainpid2.sh <<\EOF +#!/usr/bin/env bash + +set -eux +set -o pipefail + +# Create a number of children, and make one the main one +sleep infinity & +disown + +sleep infinity & +MAINPID=$! +disown + +sleep infinity & +disown + +echo $MAINPID >/run/mainpidsh2/pid +chown 1001:1001 /run/mainpidsh2/pid +EOF +chmod +x /tmp/test-mainpid2.sh + +systemd-run --unit=test-mainpidsh2.service \ + -p StandardOutput=tty \ + -p StandardError=tty \ + -p Type=forking \ + -p RuntimeDirectory=mainpidsh2 \ + -p PIDFile=/run/mainpidsh2/pid \ + /tmp/test-mainpid2.sh +test "$(systemctl show -P MainPID test-mainpidsh2.service)" -eq "$(cat /run/mainpidsh2/pid)" + +cat >/dev/shm/test-mainpid3.sh <<EOF +#!/usr/bin/env bash + +set -eux +set -o pipefail + +sleep infinity & +disown + +sleep infinity & +disown + +sleep infinity & +disown + +# Let's try to play games, and link up a privileged PID file +ln -s ../mainpidsh/pid /run/mainpidsh3/pid + +# Quick assertion that the link isn't dead +test -f /run/mainpidsh3/pid +EOF +chmod 755 /dev/shm/test-mainpid3.sh + +# This has to fail, as we shouldn't accept the dangerous PID file, and then +# inotify-wait on it to be corrected which we never do. +(! systemd-run \ + --unit=test-mainpidsh3.service \ + -p StandardOutput=tty \ + -p StandardError=tty \ + -p Type=forking \ + -p RuntimeDirectory=mainpidsh3 \ + -p PIDFile=/run/mainpidsh3/pid \ + -p DynamicUser=1 \ + `# Make sanitizers happy when DynamicUser=1 pulls in instrumented systemd NSS modules` \ + -p EnvironmentFile=-/usr/lib/systemd/systemd-asan-env \ + -p TimeoutStartSec=2s \ + /dev/shm/test-mainpid3.sh) + +# Test that this failed due to timeout, and not some other error +test "$(systemctl show -P Result test-mainpidsh3.service)" = timeout + +# Test that scope units work +systemd-run --scope --unit test-true.scope /bin/true +test "$(systemctl show -P Result test-true.scope)" = success + +# Test that user scope units work as well + +systemctl start user@4711.service +runas testuser systemd-run --scope --user --unit test-true.scope /bin/true +test "$(systemctl show -P Result test-true.scope)" = success diff --git a/test/units/testsuite-07.mount-invalid-chars.sh b/test/units/testsuite-07.mount-invalid-chars.sh new file mode 100755 index 0000000..a879334 --- /dev/null +++ b/test/units/testsuite-07.mount-invalid-chars.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Don't send invalid characters over dbus if a mount contains them + +at_exit() { + mountpoint -q /proc/1/mountinfo && umount /proc/1/mountinfo + [[ -e /tmp/fstab.bak ]] && mv -f /tmp/fstab.bak /etc/fstab + rm -f /run/systemd/system/foo-*.mount + systemctl daemon-reload +} + +trap at_exit EXIT + +# Check invalid characters directly in /proc/mountinfo +# +# This is a bit tricky (and hacky), since we have to temporarily replace +# PID 1's /proc/mountinfo, but we have to keep the original mounts intact, +# otherwise systemd would unmount them on reload +TMP_MOUNTINFO="$(mktemp)" + +cp /proc/1/mountinfo "$TMP_MOUNTINFO" +# Add a mount entry with a "Unicode non-character" in it +LANG="C.UTF-8" printf '69 1 252:2 / /foo/mountinfo rw,relatime shared:1 - cifs //foo\ufffebar rw,seclabel\n' >>"$TMP_MOUNTINFO" +mount --bind "$TMP_MOUNTINFO" /proc/1/mountinfo +systemctl daemon-reload +# On affected versions this would throw an error: +# Failed to get properties: Bad message +systemctl status foo-mountinfo.mount + +umount /proc/1/mountinfo +systemctl daemon-reload +rm -f "$TMP_MOUNTINFO" + +# Check invalid characters in a mount unit +# +# systemd already handles this and refuses to load the invalid string, e.g.: +# foo-fstab.mount:9: String is not UTF-8 clean, ignoring assignment: What=//localhost/foo���bar +# +# a) Unit generated from /etc/fstab +[[ -e /etc/fstab ]] && cp -f /etc/fstab /tmp/fstab.bak + +LANG="C.UTF-8" printf '//localhost/foo\ufffebar /foo/fstab cifs defaults 0 0\n' >/etc/fstab +systemctl daemon-reload +[[ "$(systemctl show -P UnitFileState foo-fstab.mount)" == bad ]] + +# b) Unit generated from /etc/fstab (but the invalid character is in options) +LANG="C.UTF-8" printf '//localhost/foobar /foo/fstab/opt cifs nosuid,a\ufffeb,noexec 0 0\n' >/etc/fstab +systemctl daemon-reload +[[ "$(systemctl show -P UnitFileState foo-fstab-opt.mount)" == bad ]] +rm -f /etc/fstab + +[[ -e /tmp/fstab.bak ]] && mv -f /tmp/fstab.bak /etc/fstab +systemctl daemon-reload + +# c) Mount unit +mkdir -p /run/systemd/system +LANG="C.UTF-8" printf '[Mount]\nWhat=//localhost/foo\ufffebar\nWhere=/foo/unit\nType=cifs\nOptions=noexec\n' >/run/systemd/system/foo-unit.mount +systemctl daemon-reload +[[ "$(systemctl show -P UnitFileState foo-unit.mount)" == bad ]] +rm -f /run/systemd/system/foo-unit.mount + +# d) Mount unit (but the invalid character is in Options=) +mkdir -p /run/systemd/system +LANG="C.UTF-8" printf '[Mount]\nWhat=//localhost/foobar\nWhere=/foo/unit/opt\nType=cifs\nOptions=noexec,a\ufffeb,nosuid\n' >/run/systemd/system/foo-unit-opt.mount +systemctl daemon-reload +[[ "$(systemctl show -P UnitFileState foo-unit-opt.mount)" == bad ]] +rm -f /run/systemd/system/foo-unit-opt.mount diff --git a/test/units/testsuite-07.poll-limit.sh b/test/units/testsuite-07.poll-limit.sh new file mode 100755 index 0000000..480d7ee --- /dev/null +++ b/test/units/testsuite-07.poll-limit.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-analyze log-level debug + +cat > /run/systemd/system/floodme@.service <<EOF +[Service] +ExecStart=/bin/true +EOF + +cat > /run/systemd/system/floodme.socket <<EOF +[Socket] +ListenStream=/tmp/floodme +PollLimitIntervalSec=10s +Accept=yes +PollLimitBurst=3 +EOF + +systemctl daemon-reload +systemctl start floodme.socket + +START=$(date +%s%N) + +# Trigger this 100 times in a flood +for (( i=0 ; i < 100; i++ )) ; do + logger -u /tmp/floodme foo & +done + +# Let some time pass +sleep 5 + +END=$(date +%s%N) + +PASSED=$((END-START)) + +# Calculate (round up) how many trigger events could have happened in the passed time +MAXCOUNT=$(((PASSED+10000000000)*3/10000000000)) + +# We started 100 connection attempts, but only 3 should have gone through, as per limit +test "$(systemctl show -P NAccepted floodme.socket)" -le "$MAXCOUNT" + +systemctl stop floodme.socket floodme@*.service + +rm /run/systemd/system/floodme@.service /run/systemd/system/floodme.socket /tmp/floodme + +systemctl daemon-reload diff --git a/test/units/testsuite-07.private-network.sh b/test/units/testsuite-07.private-network.sh new file mode 100755 index 0000000..37658f7 --- /dev/null +++ b/test/units/testsuite-07.private-network.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# For issue https://github.com/systemd/systemd/issues/29526 +systemd-run -p PrivateNetwork=yes --wait /bin/true diff --git a/test/units/testsuite-07.service b/test/units/testsuite-07.service new file mode 100644 index 0000000..92302bf --- /dev/null +++ b/test/units/testsuite-07.service @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-07-PID1 + +[Service] +Type=oneshot +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +NotifyAccess=all +# Issue: https://github.com/systemd/systemd/issues/2691 +ExecStop=sh -c 'kill -SEGV $$$$' +RemainAfterExit=yes +TimeoutStopSec=270s diff --git a/test/units/testsuite-07.sh b/test/units/testsuite-07.sh new file mode 100755 index 0000000..2ee1457 --- /dev/null +++ b/test/units/testsuite-07.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +# Issue: https://github.com/systemd/systemd/issues/2730 +# See TEST-07-PID1/test.sh for the first "half" of the test +mountpoint /issue2730 + +run_subtests + +touch /testok diff --git a/test/units/testsuite-08.service b/test/units/testsuite-08.service new file mode 100644 index 0000000..2db35cf --- /dev/null +++ b/test/units/testsuite-08.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-08-INITRD +After=multi-user.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-08.sh b/test/units/testsuite-08.sh new file mode 100755 index 0000000..5c6b4ce --- /dev/null +++ b/test/units/testsuite-08.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if systemd-detect-virt -qc; then + echo >&2 "This test can't run in a container" + exit 1 +fi + +# This test requires systemd to run in the initrd as well, which is not the case +# for mkinitrd-based initrd (Ubuntu/Debian) +if [[ "$(systemctl show -P InitRDTimestampMonotonic)" -eq 0 ]]; then + echo "systemd didn't run in the initrd, skipping the test" + touch /skipped + exit 0 +fi + +# We should've created a mount under /run in initrd (see the other half of the test) +# that should've survived the transition from initrd to the real system +test -d /run/initrd-mount-target +mountpoint /run/initrd-mount-target +[[ -e /run/initrd-mount-target/hello-world ]] + +# Copy the prepared shutdown initrd to its intended location. Check the respective +# test.sh file for details +mkdir -p /run/initramfs +cp -r /shutdown-initrd/* /run/initramfs/ + +touch /testok diff --git a/test/units/testsuite-09.journal.sh b/test/units/testsuite-09.journal.sh new file mode 100755 index 0000000..2ef192c --- /dev/null +++ b/test/units/testsuite-09.journal.sh @@ -0,0 +1,72 @@ +#!/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 + +get_first_boot_id() { + journalctl -b "${1:?}" -o json -n +1 | jq -r '._BOOT_ID' +} + +get_last_boot_id() { + journalctl -b "${1:?}" -o json -n 1 | jq -r '._BOOT_ID' +} + +get_first_timestamp() { + journalctl -b "${1:?}" -o json -n +1 | jq -r '.__REALTIME_TIMESTAMP' +} + +get_last_timestamp() { + journalctl -b "${1:?}" -o json -n 1 | jq -r '.__REALTIME_TIMESTAMP' +} + +# Issue: #29275, second part +# Now let's check if the boot entries are in the correct/expected order +index=0 +SYSTEMD_LOG_LEVEL=debug journalctl --list-boots +journalctl --list-boots -o json | jq -r '.[] | [.index, .boot_id, .first_entry, .last_entry] | @tsv' | + while read -r offset boot_id first_ts last_ts; do + : "Boot #$((++index)) ($offset) with ID $boot_id" + + # Try the "regular" (non-json) variants first, as they provide a helpful + # error message if something is not right + SYSTEMD_LOG_LEVEL=debug journalctl -q -n 0 -b "$index" + SYSTEMD_LOG_LEVEL=debug journalctl -q -n 0 -b "$offset" + SYSTEMD_LOG_LEVEL=debug journalctl -q -n 0 -b "$boot_id" + + # Check the boot ID of the first entry + entry_boot_id="$(get_first_boot_id "$index")" + assert_eq "$entry_boot_id" "$boot_id" + entry_boot_id="$(get_first_boot_id "$offset")" + assert_eq "$entry_boot_id" "$boot_id" + entry_boot_id="$(get_first_boot_id "$boot_id")" + assert_eq "$entry_boot_id" "$boot_id" + + # Check the timestamp of the first entry + entry_ts="$(get_first_timestamp "$index")" + assert_eq "$entry_ts" "$first_ts" + entry_ts="$(get_first_timestamp "$offset")" + assert_eq "$entry_ts" "$first_ts" + entry_ts="$(get_first_timestamp "$boot_id")" + assert_eq "$entry_ts" "$first_ts" + + # Check the boot ID of the last entry + entry_boot_id="$(get_last_boot_id "$index")" + assert_eq "$entry_boot_id" "$boot_id" + entry_boot_id="$(get_last_boot_id "$offset")" + assert_eq "$entry_boot_id" "$boot_id" + entry_boot_id="$(get_last_boot_id "$boot_id")" + assert_eq "$entry_boot_id" "$boot_id" + + # Check the timestamp of the last entry + if [[ "$offset" != "0" ]]; then + entry_ts="$(get_last_timestamp "$index")" + assert_eq "$entry_ts" "$last_ts" + entry_ts="$(get_last_timestamp "$offset")" + assert_eq "$entry_ts" "$last_ts" + entry_ts="$(get_last_timestamp "$boot_id")" + assert_eq "$entry_ts" "$last_ts" + fi + done diff --git a/test/units/testsuite-09.service b/test/units/testsuite-09.service new file mode 100644 index 0000000..6c957ec --- /dev/null +++ b/test/units/testsuite-09.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-09-REBOOT +After=multi-user.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-09.sh b/test/units/testsuite-09.sh new file mode 100755 index 0000000..cd95660 --- /dev/null +++ b/test/units/testsuite-09.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +NUM_REBOOT=4 + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +systemd-cat echo "Reboot count: $REBOOT_COUNT" +systemd-cat journalctl --list-boots + +run_subtests + +if [[ "$REBOOT_COUNT" -lt "$NUM_REBOOT" ]]; then + systemctl_final reboot +elif [[ "$REBOOT_COUNT" -gt "$NUM_REBOOT" ]]; then + assert_not_reached +fi + +touch /testok diff --git a/test/units/testsuite-13.machinectl.sh b/test/units/testsuite-13.machinectl.sh new file mode 100755 index 0000000..b5f90f6 --- /dev/null +++ b/test/units/testsuite-13.machinectl.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +export PAGER= + +at_exit() { + set +e + + 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 +} + +trap at_exit EXIT + +# Mount tmpfs over /var/lib/machines to not pollute the image +mkdir -p /var/lib/machines +mount -t tmpfs tmpfs /var/lib/machines + +# Create a couple of containers we can refer to in tests +for i in {0..4}; do + create_dummy_container "/var/lib/machines/container$i" + machinectl start "container$i" +done +# Create one "long running" container with some basic signal handling +create_dummy_container /var/lib/machines/long-running +cat >/var/lib/machines/long-running/sbin/init <<\EOF +#!/usr/bin/bash -x + +PID=0 + +trap "touch /poweroff" RTMIN+4 +trap "touch /reboot" INT +trap "touch /trap" TRAP +trap 'kill $PID' EXIT + +# We need to wait for the sleep process asynchronously in order to allow +# bash to process signals +sleep infinity & +PID=$! +while :; do + wait || : +done +EOF +machinectl start long-running + +machinectl +machinectl --no-pager --help +machinectl --version +machinectl list +machinectl list --no-legend --no-ask-password + +machinectl status long-running long-running long-running +machinectl status --full long-running +machinectl status --quiet --lines=1 long-running +machinectl status --lines=0 --max-addresses=0 long-running +machinectl status --machine=testuser@.host long-running +machinectl status --output=help long-running +while read -r output; do + machinectl status --output="$output" long-running +done < <(machinectl --output=help) + +machinectl show +machinectl show --all +machinectl show --all --machine=root@ +machinectl show --all --machine=testuser@ +[[ "$(machinectl show --property=PoolPath --value)" == "/var/lib/machines" ]] +machinectl show long-running +machinectl show long-running long-running long-running --all +[[ "$(machinectl show --property=RootDirectory --value long-running)" == "/var/lib/machines/long-running" ]] + +machinectl enable long-running +test -L /etc/systemd/system/machines.target.wants/systemd-nspawn@long-running.service +machinectl enable long-running long-running long-running container1 +machinectl disable long-running +test ! -L /etc/systemd/system/machines.target.wants/systemd-nspawn@long-running.service +machinectl disable long-running long-running long-running container1 + +[[ "$(machinectl shell testuser@ /usr/bin/bash -c 'echo -ne $FOO')" == "" ]] +[[ "$(machinectl shell --setenv=FOO=bar testuser@ /usr/bin/bash -c 'echo -ne $FOO')" == "bar" ]] + +[[ "$(machinectl show --property=State --value long-running)" == "running" ]] +# Equivalent to machinectl kill --signal=SIGRTMIN+4 --kill-whom=leader +rm -f /var/lib/machines/long-running/poweroff +machinectl poweroff long-running +timeout 10 bash -c "until test -e /var/lib/machines/long-running/poweroff; do sleep .5; done" +# Equivalent to machinectl kill --signal=SIGINT --kill-whom=leader +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" +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" +# Multiple machines at once +machinectl poweroff long-running long-running long-running +machinectl reboot long-running long-running long-running +machinectl kill --signal=SIGTRAP --kill-whom=leader long-running long-running long-running +# All used signals should've been caught by a handler +[[ "$(machinectl show --property=State --value long-running)" == "running" ]] + +cp /etc/machine-id /tmp/foo +machinectl copy-to long-running /tmp/foo /root/foo +test -f /var/lib/machines/long-running/root/foo +machinectl copy-from long-running /root/foo /tmp/bar +diff /tmp/foo /tmp/bar +rm -f /tmp/{foo,bar} + +# machinectl bind is covered by testcase_check_machinectl_bind() in nspawn tests + +machinectl list-images +machinectl list-images --no-legend +machinectl image-status +machinectl image-status container1 +machinectl image-status container1 container1 container{0..4} +machinectl show-image +machinectl show-image container1 +machinectl show-image container1 container1 container{0..4} + +machinectl clone container1 clone1 +machinectl show-image clone1 +machinectl rename clone1 clone2 +(! machinectl show-image clone1) +machinectl show-image clone2 +if lsattr -d /var/lib/machines >/dev/null; then + [[ "$(machinectl show-image --property=ReadOnly --value clone2)" == no ]] + machinectl read-only clone2 yes + [[ "$(machinectl show-image --property=ReadOnly --value clone2)" == yes ]] + machinectl read-only clone2 no + [[ "$(machinectl show-image --property=ReadOnly --value clone2)" == no ]] +fi +machinectl remove clone2 +for i in {0..4}; do + machinectl clone container1 "clone$i" +done +machinectl remove clone{0..4} +for i in {0..4}; do + machinectl clone container1 ".hidden$i" +done +machinectl list-images --all +test -d /var/lib/machines/.hidden1 +machinectl clean +test ! -d /var/lib/machines/.hidden1 + +# Prepare a simple raw container +mkdir -p /tmp/mnt +dd if=/dev/zero of=/tmp/container.raw bs=1M count=64 +mkfs.ext4 /tmp/container.raw +mount -o loop /tmp/container.raw /tmp/mnt +cp -r /var/lib/machines/container1/* /tmp/mnt +umount /tmp/mnt +# Try to import it, run it, export it, and re-import it +machinectl import-raw /tmp/container.raw container-raw +[[ "$(machinectl show-image --property=Type --value container-raw)" == "raw" ]] +machinectl start container-raw +machinectl export-raw container-raw /tmp/container-export.raw +machinectl import-raw /tmp/container-export.raw container-raw-reimport +[[ "$(machinectl show-image --property=Type --value container-raw-reimport)" == "raw" ]] +rm -f /tmp/container{,-export}.raw + +# Prepare a simple tar.gz container +tar -pczf /tmp/container.tar.gz -C /var/lib/machines/container1 . +# Try to import it, run it, export it, and re-import it +machinectl import-tar /tmp/container.tar.gz container-tar +[[ "$(machinectl show-image --property=Type --value container-tar)" == "directory" ]] +machinectl start container-tar +machinectl export-tar container-tar /tmp/container-export.tar.gz +machinectl import-tar /tmp/container-export.tar.gz container-tar-reimport +[[ "$(machinectl show-image --property=Type --value container-tar-reimport)" == "directory" ]] +rm -f /tmp/container{,-export}.tar.gz + +# Try to import a container directory & run it +cp -r /var/lib/machines/container1 /tmp/container.dir +machinectl import-fs /tmp/container.dir container-dir +[[ "$(machinectl show-image --property=Type --value container-dir)" == "directory" ]] +machinectl start container-dir +rm -fr /tmp/container.dir + +timeout 10 bash -c "until machinectl clean --all; do sleep .5; done" + +NSPAWN_FRAGMENT="machinectl-test-$RANDOM.nspawn" +cat >"/var/lib/machines/$NSPAWN_FRAGMENT" <<EOF +[Exec] +Boot=true +EOF +machinectl cat "$NSPAWN_FRAGMENT" +EDITOR=true script -qec "machinectl edit $NSPAWN_FRAGMENT" /dev/null +test -f "/etc/systemd/nspawn/$NSPAWN_FRAGMENT" +diff "/var/lib/machines/$NSPAWN_FRAGMENT" "/etc/systemd/nspawn/$NSPAWN_FRAGMENT" + +cat >/tmp/fragment.nspawn <<EOF +[Exec] +Boot=false +EOF +machinectl cat /tmp/fragment.nspawn +EDITOR="cp /tmp/fragment.nspawn" script -qec "machinectl edit $NSPAWN_FRAGMENT" /dev/null +diff /tmp/fragment.nspawn "/etc/systemd/nspawn/$NSPAWN_FRAGMENT" + +for opt in format lines machine max-addresses output setenv verify; do + (! machinectl status "--$opt=" long-running) + (! machinectl status "--$opt=-1" long-running) + (! machinectl status "--$opt=''" long-running) +done +(! machinectl show "") +(! machinectl enable) +(! machinectl enable "") +(! machinectl disable) +(! machinectl disable "") +(! machinectl read-only container1 "") +(! machinectl read-only container1 foo) +(! machinectl read-only container1 -- -1) diff --git a/test/units/testsuite-13.nspawn-oci.sh b/test/units/testsuite-13.nspawn-oci.sh new file mode 100755 index 0000000..8fa0bc4 --- /dev/null +++ b/test/units/testsuite-13.nspawn-oci.sh @@ -0,0 +1,467 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +export SYSTEMD_LOG_LEVEL=debug +export SYSTEMD_LOG_TARGET=journal + +# shellcheck disable=SC2317 +at_exit() { + set +e + + mountpoint -q /var/lib/machines && umount /var/lib/machines + [[ -n "${DEV:-}" ]] && rm -f "$DEV" + [[ -n "${NETNS:-}" ]] && umount "$NETNS" && rm -f "$NETNS" + [[ -n "${TMPDIR:-}" ]] && rm -fr "$TMPDIR" + rm -f /run/systemd/nspawn/*.nspawn +} + +trap at_exit EXIT + +# Mount tmpfs over /var/lib/machines to not pollute the image +mkdir -p /var/lib/machines +mount -t tmpfs tmpfs /var/lib/machines + +# Setup a couple of dirs/devices for the OCI containers +DEV="$(mktemp -u /dev/oci-dev-XXX)" +mknod -m 666 "$DEV" b 42 42 +NETNS="$(mktemp /var/tmp/netns.XXX)" +mount --bind /proc/self/ns/net "$NETNS" +TMPDIR="$(mktemp -d)" +touch "$TMPDIR/hello" +OCI="$(mktemp -d /var/lib/machines/testsuite-13.oci-bundle.XXX)" +create_dummy_container "$OCI/rootfs" +mkdir -p "$OCI/rootfs/opt/var" +mkdir -p "$OCI/rootfs/opt/readonly" + +# Let's start with a simple config +cat >"$OCI/config.json" <<EOF +{ + "ociVersion" : "1.0.0", + "root" : { + "path" : "rootfs" + }, + "mounts" : [ + { + "destination" : "/root", + "type" : "tmpfs", + "source" : "tmpfs" + } + ] +} +EOF +systemd-nspawn --oci-bundle="$OCI" bash -xec 'mountpoint /root' + +# And now for something a bit more involved +# Notes: +# - the hooks are parsed & processed, but never executed +# - set sysctl's are parsed but never used? +# - same goes for arg_sysctl in nspawn.c +cat >"$OCI/config.json" <<EOF +{ + "ociVersion" : "1.0.0", + "hostname" : "my-oci-container", + "root" : { + "path" : "rootfs", + "readonly" : false + }, + "mounts" : [ + { + "destination" : "/root", + "type" : "tmpfs", + "source" : "tmpfs" + }, + ${COVERAGE_BUILD_DIR:+"{ \"destination\" : \"$COVERAGE_BUILD_DIR\" },"} + { + "destination" : "/var", + "type" : "none", + "source" : "$TMPDIR", + "options" : ["rbind", "rw"] + } + ], + "process" : { + "terminal" : false, + "consoleSize" : { + "height" : 25, + "width" : 80 + }, + "user" : { + "uid" : 0, + "gid" : 0, + "additionalGids" : [5, 6] + }, + "env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "FOO=bar" + ], + "cwd" : "/root", + "args" : [ + "bash", + "-xe", + "/entrypoint.sh" + ], + "noNewPrivileges" : true, + "oomScoreAdj" : 20, + "capabilities" : { + "bounding" : [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "permitted" : [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "inheritable" : [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "effective" : [ + "CAP_AUDIT_WRITE", + "CAP_KILL" + ], + "ambient" : [ + "CAP_NET_BIND_SERVICE" + ] + }, + "rlimits" : [ + { + "type" : "RLIMIT_NOFILE", + "soft" : 1024, + "hard" : 1024 + }, + { + "type" : "RLIMIT_RTPRIO", + "soft" : 5, + "hard" : 10 + } + ] + }, + "linux" : { + "namespaces" : [ + { + "type" : "mount" + }, + { + "type" : "network", + "path" : "$NETNS" + }, + { + "type" : "pid" + }, + { + "type" : "uts" + } + ], + "uidMappings" : [ + { + "containerID" : 0, + "hostID" : 1000, + "size" : 100 + } + ], + "gidMappings" : [ + { + "containerID" : 0, + "hostID" : 1000, + "size" : 100 + } + ], + "devices" : [ + { + "type" : "c", + "path" : "/dev/zero", + "major" : 1, + "minor" : 5, + "fileMode" : 444 + }, + { + "type" : "b", + "path" : "$DEV", + "major" : 4, + "minor" : 2, + "fileMode" : 666, + "uid" : 0, + "gid" : 0 + } + ], + "resources" : { + "devices" : [ + { + "allow" : false, + "access" : "m" + }, + { + "allow" : true, + "type" : "b", + "major" : 4, + "minor" : 2, + "access" : "rwm" + } + ], + "memory" : { + "limit" : 134217728, + "reservation" : 33554432, + "swap" : 268435456 + }, + "cpu" : { + "shares" : 1024, + "quota" : 1000000, + "period" : 500000, + "cpus" : "0-7" + }, + "blockIO" : { + "weight" : 10, + "weightDevice" : [ + { + "major" : 4, + "minor" : 2, + "weight" : 500 + } + ], + "throttleReadBpsDevice" : [ + { + "major" : 4, + "minor" : 2, + "rate" : 500 + } + ], + "throttleWriteBpsDevice" : [ + { + "major" : 4, + "minor" : 2, + "rate" : 500 + } + ], + "throttleReadIOPSDevice" : [ + { + "major" : 4, + "minor" : 2, + "rate" : 500 + } + ], + "throttleWriteIOPSDevice" : [ + { + "major" : 4, + "minor" : 2, + "rate" : 500 + } + ] + }, + "pids" : { + "limit" : 1024 + } + }, + "sysctl" : { + "kernel.domainname" : "foo.bar", + "vm.swappiness" : "60" + }, + "seccomp" : { + "defaultAction" : "SCMP_ACT_ALLOW", + "architectures" : [ + "SCMP_ARCH_ARM", + "SCMP_ARCH_X86_64" + ], + "syscalls" : [ + { + "names" : [ + "lchown", + "chmod" + ], + "action" : "SCMP_ACT_ERRNO", + "args" : [ + { + "index" : 0, + "value" : 1, + "op" : "SCMP_CMP_NE" + }, + { + "index" : 1, + "value" : 2, + "valueTwo" : 3, + "op" : "SCMP_CMP_MASKED_EQ" + } + ] + } + ] + }, + "rootfsPropagation" : "shared", + "maskedPaths" : [ + "/proc/kcore", + "/root/nonexistent" + ], + "readonlyPaths" : [ + "/proc/sys", + "/opt/readonly" + ] + }, + "hooks" : { + "prestart" : [ + { + "path" : "/bin/sh", + "args" : [ + "-xec", + "echo \$PRESTART_FOO >/prestart" + ], + "env" : [ + "PRESTART_FOO=prestart_bar", + "ALSO_FOO=also_bar" + ], + "timeout" : 666 + }, + { + "path" : "/bin/touch", + "args" : [ + "/tmp/also-prestart" + ] + } + ], + "poststart" : [ + { + "path" : "/bin/sh", + "args" : [ + "touch", + "/poststart" + ] + } + ], + "poststop" : [ + { + "path" : "/bin/sh", + "args" : [ + "touch", + "/poststop" + ] + } + ] + }, + "annotations" : { + "hello.world" : "1", + "foo" : "bar" + } +} +EOF +# Create a simple "entrypoint" script that validates that the container +# is created correctly according to the OCI config +cat >"$OCI/rootfs/entrypoint.sh" <<EOF +#!/usr/bin/bash -e + +# Mounts +mountpoint /root +mountpoint /var +test -e /var/hello + +# Process +[[ "\$PWD" == /root ]] +[[ "\$FOO" == bar ]] + +# Process - rlimits +[[ "\$(ulimit -S -n)" -eq 1024 ]] +[[ "\$(ulimit -H -n)" -eq 1024 ]] +[[ "\$(ulimit -S -r)" -eq 5 ]] +[[ "\$(ulimit -H -r)" -eq 10 ]] +[[ "\$(hostname)" == my-oci-container ]] + +# Linux - devices +test -c /dev/zero +test -b "$DEV" +[[ "\$(stat -c '%t:%T' "$DEV")" == 4:2 ]] + +# Linux - maskedPaths +test -e /proc/kcore +cat /proc/kcore && exit 1 +test ! -e /root/nonexistent + +# Linux - readonlyPaths +touch /opt/readonly/foo && exit 1 + +exit 0 +EOF +timeout 30 systemd-nspawn --oci-bundle="$OCI" + +# Test a couple of invalid configs +INVALID_SNIPPETS=( + # Invalid object + '"foo" : { }' + '"process" : { "foo" : [ ] }' + # Non-absolute mount + '"mounts" : [ { "destination" : "foo", "type" : "tmpfs", "source" : "tmpfs" } ]' + # Invalid rlimit + '"process" : { "rlimits" : [ { "type" : "RLIMIT_FOO", "soft" : 0, "hard" : 0 } ] }' + # rlimit without RLIMIT_ prefix + '"process" : { "rlimits" : [ { "type" : "CORE", "soft" : 0, "hard" : 0 } ] }' + # Invalid env assignment + '"process" : { "env" : [ "foo" ] }' + '"process" : { "env" : [ "foo=bar", 1 ] }' + # Invalid process args + '"process" : { "args" : [ ] }' + '"process" : { "args" : [ "" ] }' + '"process" : { "args" : [ "foo", 1 ] }' + # Invalid capabilities + '"process" : { "capabilities" : { "bounding" : [ 1 ] } }' + '"process" : { "capabilities" : { "bounding" : [ "FOO_BAR" ] } }' + # Unsupported option (without JSON_PERMISSIVE) + '"linux" : { "resources" : { "cpu" : { "realtimeRuntime" : 1 } } }' + # Invalid namespace + '"linux" : { "namespaces" : [ { "type" : "foo" } ] }' + # Namespace path for a non-network namespace + '"linux" : { "namespaces" : [ { "type" : "user", "path" : "/foo/bar" } ] }' + # Duplicate namespace + '"linux" : { "namespaces" : [ { "type" : "ipc" }, { "type" : "ipc" } ] }' + # Invalid device type + '"linux" : { "devices" : [ { "type" : "foo", "path" : "/dev/foo" } ] }' + # Invalid cgroups path + '"linux" : { "cgroupsPath" : "/foo/bar/baz" }' + '"linux" : { "cgroupsPath" : "foo/bar/baz" }' + # Invalid sysctl assignments + '"linux" : { "sysctl" : { "vm.swappiness" : 60 } }' + '"linux" : { "sysctl" : { "foo..bar" : "baz" } }' + # Invalid seccomp assignments + '"linux" : { "seccomp" : { } }' + '"linux" : { "seccomp" : { "defaultAction" : 1 } }' + '"linux" : { "seccomp" : { "defaultAction" : "foo" } }' + '"linux" : { "seccomp" : { "defaultAction" : "SCMP_ACT_ALLOW", "syscalls" : [ { "action" : "SCMP_ACT_ERRNO", "names" : [ ] } ] } }' + # Invalid masked paths + '"linux" : { "maskedPaths" : [ "/foo", 1 ] }' + '"linux" : { "maskedPaths" : [ "/foo", "bar" ] }' + # Invalid read-only paths + '"linux" : { "readonlyPaths" : [ "/foo", 1 ] }' + '"linux" : { "readonlyPaths" : [ "/foo", "bar" ] }' + # Invalid hooks + '"hooks" : { "prestart" : [ { "path" : "/bin/sh", "timeout" : 0 } ] }' + # Invalid annotations + '"annotations" : { "" : "bar" }' + '"annotations" : { "foo" : 1 }' +) + +for snippet in "${INVALID_SNIPPETS[@]}"; do + : "Snippet: $snippet" + cat >"$OCI/config.json" <<EOF +{ + "ociVersion" : "1.0.0", + "root" : { + "path" : "rootfs" + }, + $snippet +} +EOF + (! systemd-nspawn --oci-bundle="$OCI" sh -c 'echo hello') +done + +# Invalid OCI bundle version +cat >"$OCI/config.json" <<EOF +{ + "ociVersion" : "6.6.6", + "root" : { + "path" : "rootfs" + } +} +EOF +(! systemd-nspawn --oci-bundle="$OCI" sh -c 'echo hello') diff --git a/test/units/testsuite-13.nspawn.sh b/test/units/testsuite-13.nspawn.sh new file mode 100755 index 0000000..01f6eb6 --- /dev/null +++ b/test/units/testsuite-13.nspawn.sh @@ -0,0 +1,884 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +# +# Notes on coverage: when collecting coverage we need the $BUILD_DIR present +# and writable in the container as well. To do this in the least intrusive way, +# two things are going on in the background (only when built with -Db_coverage=true): +# 1) the systemd-nspawn@.service is copied to /etc/systemd/system/ with +# --bind=$BUILD_DIR appended to the ExecStart= line +# 2) each create_dummy_container() call also creates an .nspawn file in /run/systemd/nspawn/ +# with the last fragment from the path used as a name +# +# The first change is quite self-contained and applies only to containers run +# with machinectl. The second one might cause some unexpected side-effects, namely: +# - nspawn config (setting) files don't support dropins, so tests that test +# the config files might need some tweaking (as seen below with +# the $COVERAGE_BUILD_DIR shenanigans) since they overwrite the .nspawn file +# - also a note - if /etc/systemd/nspawn/cont-name.nspawn exists, it takes +# precedence and /run/systemd/nspawn/cont-name.nspawn won't be read even +# if it exists +# - also a note 2 - --bind= overrides any Bind= from a config file +# - in some cases we don't create a test container using create_dummy_container(), +# so in that case an explicit call to coverage_create_nspawn_dropin() is needed +# +# However, even after jumping through all these hooks, there still might (and is) +# some "incorrectly" missing coverage, especially in the window between spawning +# the inner child process and bind-mounting the coverage $BUILD_DIR +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + + +export SYSTEMD_LOG_LEVEL=debug +export SYSTEMD_LOG_TARGET=journal + +at_exit() { + set +e + + mountpoint -q /var/lib/machines && umount --recursive /var/lib/machines + rm -f /run/systemd/nspawn/*.nspawn +} + +trap at_exit EXIT + +# check cgroup-v2 +IS_CGROUPSV2_SUPPORTED=no +mkdir -p /tmp/cgroup2 +if mount -t cgroup2 cgroup2 /tmp/cgroup2; then + IS_CGROUPSV2_SUPPORTED=yes + umount /tmp/cgroup2 +fi +rmdir /tmp/cgroup2 + +# check cgroup namespaces +IS_CGNS_SUPPORTED=no +if [[ -f /proc/1/ns/cgroup ]]; then + IS_CGNS_SUPPORTED=yes +fi + +IS_USERNS_SUPPORTED=no +# On some systems (e.g. CentOS 7) the default limit for user namespaces +# is set to 0, which causes the following unshare syscall to fail, even +# with enabled user namespaces support. By setting this value explicitly +# we can ensure the user namespaces support to be detected correctly. +sysctl -w user.max_user_namespaces=10000 +if unshare -U bash -c :; then + IS_USERNS_SUPPORTED=yes +fi + +# Mount tmpfs over /var/lib/machines to not pollute the image +mkdir -p /var/lib/machines +mount -t tmpfs tmpfs /var/lib/machines + +testcase_sanity() { + local template root image uuid tmpdir + + tmpdir="$(mktemp -d)" + template="$(mktemp -d /tmp/nspawn-template.XXX)" + create_dummy_container "$template" + # Create a simple image from the just created container template + image="$(mktemp /var/lib/machines/testsuite-13.image-XXX.img)" + dd if=/dev/zero of="$image" bs=1M count=64 + mkfs.ext4 "$image" + mkdir -p /mnt + mount -o loop "$image" /mnt + cp -r "$template"/* /mnt/ + umount /mnt + + systemd-nspawn --help --no-pager + systemd-nspawn --version + + # --template= + root="$(mktemp -u -d /var/lib/machines/testsuite-13.sanity.XXX)" + coverage_create_nspawn_dropin "$root" + (! systemd-nspawn --directory="$root" bash -xec 'echo hello') + # Initialize $root from $template (the $root directory must not exist, hence + # the `mktemp -u` above) + systemd-nspawn --directory="$root" --template="$template" bash -xec 'echo hello' + systemd-nspawn --directory="$root" bash -xec 'echo hello; touch /initialized' + test -e "$root/initialized" + # Check if the $root doesn't get re-initialized once it's not empty + systemd-nspawn --directory="$root" --template="$template" bash -xec 'echo hello' + test -e "$root/initialized" + + systemd-nspawn --directory="$root" --ephemeral bash -xec 'touch /ephemeral' + test ! -e "$root/ephemeral" + (! systemd-nspawn --directory="$root" \ + --read-only \ + bash -xec 'touch /nope') + test ! -e "$root/nope" + systemd-nspawn --image="$image" bash -xec 'echo hello' + + # --volatile= + touch "$root/usr/has-usr" + # volatile(=yes): rootfs is tmpfs, /usr/ from the OS tree is mounted read only + systemd-nspawn --directory="$root"\ + --volatile \ + bash -xec 'test -e /usr/has-usr; touch /usr/read-only && exit 1; touch /nope' + test ! -e "$root/nope" + test ! -e "$root/usr/read-only" + systemd-nspawn --directory="$root"\ + --volatile=yes \ + bash -xec 'test -e /usr/has-usr; touch /usr/read-only && exit 1; touch /nope' + test ! -e "$root/nope" + test ! -e "$root/usr/read-only" + # volatile=state: rootfs is read-only, /var/ is tmpfs + systemd-nspawn --directory="$root" \ + --volatile=state \ + bash -xec 'test -e /usr/has-usr; mountpoint /var; touch /read-only && exit 1; touch /var/nope' + test ! -e "$root/read-only" + test ! -e "$root/var/nope" + # volatile=state: tmpfs overlay is mounted over rootfs + systemd-nspawn --directory="$root" \ + --volatile=overlay \ + bash -xec 'test -e /usr/has-usr; touch /nope; touch /var/also-nope; touch /usr/nope-too' + test ! -e "$root/nope" + test ! -e "$root/var/also-nope" + test ! -e "$root/usr/nope-too" + + # --machine=, --hostname= + systemd-nspawn --directory="$root" \ + --machine="foo-bar.baz" \ + bash -xec '[[ $(hostname) == foo-bar.baz ]]' + systemd-nspawn --directory="$root" \ + --hostname="hello.world.tld" \ + bash -xec '[[ $(hostname) == hello.world.tld ]]' + systemd-nspawn --directory="$root" \ + --machine="foo-bar.baz" \ + --hostname="hello.world.tld" \ + bash -xec '[[ $(hostname) == hello.world.tld ]]' + + # --uuid= + rm -f "$root/etc/machine-id" + uuid="deadbeef-dead-dead-beef-000000000000" + systemd-nspawn --directory="$root" \ + --uuid="$uuid" \ + bash -xec "[[ \$container_uuid == $uuid ]]" + + # --as-pid2 + systemd-nspawn --directory="$root" bash -xec '[[ $$ -eq 1 ]]' + systemd-nspawn --directory="$root" --as-pid2 bash -xec '[[ $$ -eq 2 ]]' + + # --user= + # "Fake" getent passwd's bare minimum, so we don't have to pull it in + # with all the DSO shenanigans + cat >"$root/bin/getent" <<\EOF +#!/bin/bash + +if [[ $# -eq 0 ]]; then + : +elif [[ $1 == passwd ]]; then + echo "testuser:x:1000:1000:testuser:/:/bin/sh" +elif [[ $1 == initgroups ]]; then + echo "testuser" +fi +EOF + chmod +x "$root/bin/getent" + systemd-nspawn --directory="$root" bash -xec '[[ $USER == root ]]' + systemd-nspawn --directory="$root" --user=testuser bash -xec '[[ $USER == testuser ]]' + + # --settings= + .nspawn files + mkdir -p /run/systemd/nspawn/ + uuid="deadbeef-dead-dead-beef-000000000000" + echo -ne "[Exec]\nMachineID=deadbeef-dead-dead-beef-111111111111" >/run/systemd/nspawn/foo-bar.nspawn + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --settings=yes \ + bash -xec '[[ $container_uuid == deadbeef-dead-dead-beef-111111111111 ]]' + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --uuid="$uuid" \ + --settings=yes \ + bash -xec "[[ \$container_uuid == $uuid ]]" + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --uuid="$uuid" \ + --settings=override \ + bash -xec '[[ $container_uuid == deadbeef-dead-dead-beef-111111111111 ]]' + systemd-nspawn --directory="$root" \ + --machine=foo-bar \ + --uuid="$uuid" \ + --settings=trusted \ + bash -xec "[[ \$container_uuid == $uuid ]]" + + # Mounts + mkdir "$tmpdir"/{1,2,3} + touch "$tmpdir/1/one" "$tmpdir/2/two" "$tmpdir/3/three" + touch "$tmpdir/foo" + # --bind= + systemd-nspawn --directory="$root" \ + ${COVERAGE_BUILD_DIR:+--bind="$COVERAGE_BUILD_DIR"} \ + --bind="$tmpdir:/foo" \ + --bind="$tmpdir:/also-foo:noidmap,norbind" \ + bash -xec 'test -e /foo/foo; touch /foo/bar; test -e /also-foo/bar' + test -e "$tmpdir/bar" + # --bind-ro= + systemd-nspawn --directory="$root" \ + --bind-ro="$tmpdir:/foo" \ + --bind-ro="$tmpdir:/bar:noidmap,norbind" \ + bash -xec 'test -e /foo/foo; touch /foo/baz && exit 1; touch /bar && exit 1; true' + # --inaccessible= + systemd-nspawn --directory="$root" \ + --inaccessible=/var \ + bash -xec 'touch /var/foo && exit 1; true' + # --tmpfs= + systemd-nspawn --directory="$root" \ + --tmpfs=/var:rw,nosuid,noexec \ + bash -xec 'touch /var/nope' + test ! -e "$root/var/nope" + # --overlay= + systemd-nspawn --directory="$root" \ + --overlay="$tmpdir/1:$tmpdir/2:$tmpdir/3:/var" \ + bash -xec 'test -e /var/one; test -e /var/two; test -e /var/three; touch /var/foo' + test -e "$tmpdir/3/foo" + # --overlay-ro= + systemd-nspawn --directory="$root" \ + --overlay-ro="$tmpdir/1:$tmpdir/2:$tmpdir/3:/var" \ + bash -xec 'test -e /var/one; test -e /var/two; test -e /var/three; touch /var/nope && exit 1; true' + test ! -e "$tmpdir/3/nope" + rm -fr "$tmpdir" + + # --port (sanity only) + systemd-nspawn --network-veth --directory="$root" --port=80 --port=90 true + systemd-nspawn --network-veth --directory="$root" --port=80:8080 true + systemd-nspawn --network-veth --directory="$root" --port=tcp:80 true + systemd-nspawn --network-veth --directory="$root" --port=tcp:80:8080 true + systemd-nspawn --network-veth --directory="$root" --port=udp:80 true + systemd-nspawn --network-veth --directory="$root" --port=udp:80:8080 --port=tcp:80:8080 true + (! systemd-nspawn --network-veth --directory="$root" --port= true) + (! systemd-nspawn --network-veth --directory="$root" --port=-1 true) + (! systemd-nspawn --network-veth --directory="$root" --port=: true) + (! systemd-nspawn --network-veth --directory="$root" --port=icmp:80:8080 true) + (! systemd-nspawn --network-veth --directory="$root" --port=tcp::8080 true) + (! systemd-nspawn --network-veth --directory="$root" --port=8080: true) + # Exercise adding/removing ports from an interface + systemd-nspawn --directory="$root" \ + --network-veth \ + --port=6667 \ + --port=80:8080 \ + --port=udp:53 \ + --port=tcp:22:2222 \ + bash -xec 'ip addr add dev host0 10.0.0.10/24; ip a; ip addr del dev host0 10.0.0.10/24' + + # --load-credential=, --set-credential= + echo "foo bar" >/tmp/cred.path + systemd-nspawn --directory="$root" \ + --load-credential=cred.path:/tmp/cred.path \ + --set-credential="cred.set:hello world" \ + bash -xec '[[ "$(</run/host/credentials/cred.path)" == "foo bar" ]]; [[ "$(</run/host/credentials/cred.set)" == "hello world" ]]' + rm -f /tmp/cred.path + + # Assorted tests + systemd-nspawn --directory="$root" --suppress-sync=yes bash -xec 'echo hello' + systemd-nspawn --capability=help + systemd-nspawn --resolv-conf=help + systemd-nspawn --timezone=help + + # Handling of invalid arguments + opts=( + bind + bind-ro + bind-user + chdir + console + inaccessible + kill-signal + link-journal + load-credential + network-{interface,macvlan,ipvlan,veth-extra,bridge,zone} + no-new-privileges + oom-score-adjust + overlay + overlay-ro + personality + pivot-root + port + private-users + private-users-ownership + register + resolv-conf + rlimit + root-hash + root-hash-sig + set-credential + settings + suppress-sync + timezone + tmpfs + uuid + ) + for opt in "${opts[@]}"; do + (! systemd-nspawn "--$opt") + [[ "$opt" == network-zone ]] && continue + (! systemd-nspawn "--$opt=''") + (! systemd-nspawn "--$opt=%\$š") + done + (! systemd-nspawn --volatile="") + (! systemd-nspawn --volatile=-1) + (! systemd-nspawn --rlimit==) +} + +nspawn_settings_cleanup() { + for dev in sd-host-only sd-shared{1,2} sd-macvlan{1,2} sd-ipvlan{1,2}; do + ip link del "$dev" || : + done + + return 0 +} + +testcase_nspawn_settings() { + local root container dev private_users + + mkdir -p /run/systemd/nspawn + root="$(mktemp -d /var/lib/machines/testsuite-13.nspawn-settings.XXX)" + container="$(basename "$root")" + create_dummy_container "$root" + rm -f "/etc/systemd/nspawn/$container.nspawn" + mkdir -p "$root/tmp" "$root"/opt/{tmp,inaccessible,also-inaccessible} + + for dev in sd-host-only sd-shared{1,2} sd-macvlan{1,2} sd-macvlanloong sd-ipvlan{1,2} sd-ipvlanlooong; do + ip link add "$dev" type dummy + done + udevadm settle + ip link + trap nspawn_settings_cleanup RETURN + + # Let's start with one huge config to test as much as we can at once + cat >"/run/systemd/nspawn/$container.nspawn" <<EOF +[Exec] +Boot=no +Ephemeral=no +ProcessTwo=no +Parameters=bash /entrypoint.sh "foo bar" 'bar baz' +Environment=FOO=bar +Environment=BAZ="hello world" +User=root +WorkingDirectory=/tmp +Capability=CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN +DropCapability=CAP_AUDIT_CONTROL CAP_AUDIT_WRITE +AmbientCapability=CAP_BPF CAP_CHOWN +NoNewPrivileges=no +MachineID=f28f129b51874b1280a89421ec4b4ad4 +PrivateUsers=no +NotifyReady=no +SystemCallFilter=@basic-io @chown +SystemCallFilter=~ @clock +LimitNOFILE=1024:2048 +LimitRTPRIO=8:16 +OOMScoreAdjust=32 +CPUAffinity=0,0-5,1-5 +Hostname=nspawn-settings +ResolvConf=copy-host +Timezone=delete +LinkJournal=no +SuppressSync=no + +[Files] +ReadOnly=no +Volatile=no +TemporaryFileSystem=/tmp +TemporaryFileSystem=/opt/tmp +Inaccessible=/opt/inaccessible +Inaccessible=/opt/also-inaccessible +PrivateUsersOwnership=auto +Overlay=+/var::/var +${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"} + +[Network] +Private=yes +VirtualEthernet=yes +VirtualEthernetExtra=my-fancy-veth1 +VirtualEthernetExtra=fancy-veth2:my-fancy-veth2 +Interface=sd-shared1 sd-shared2:sd-shared2 +MACVLAN=sd-macvlan1 sd-macvlan2:my-macvlan2 sd-macvlanloong +IPVLAN=sd-ipvlan1 sd-ipvlan2:my-ipvlan2 sd-ipvlanlooong +Zone=sd-zone0 +Port=80 +Port=81:8181 +Port=tcp:60 +Port=udp:60:61 +EOF + cat >"$root/entrypoint.sh" <<\EOF +#!/bin/bash +set -ex + +env + +[[ "$1" == "foo bar" ]] +[[ "$2" == "bar baz" ]] + +[[ "$USER" == root ]] +[[ "$FOO" == bar ]] +[[ "$BAZ" == "hello world" ]] +[[ "$PWD" == /tmp ]] +[[ "$container_uuid" == f28f129b-5187-4b12-80a8-9421ec4b4ad4 ]] +[[ "$(ulimit -S -n)" -eq 1024 ]] +[[ "$(ulimit -H -n)" -eq 2048 ]] +[[ "$(ulimit -S -r)" -eq 8 ]] +[[ "$(ulimit -H -r)" -eq 16 ]] +[[ "$(</proc/self/oom_score_adj)" -eq 32 ]] +[[ "$(hostname)" == nspawn-settings ]] +[[ -e /etc/resolv.conf ]] +[[ ! -e /etc/localtime ]] + +mountpoint /tmp +touch /tmp/foo +mountpoint /opt/tmp +touch /opt/tmp/foo +touch /opt/inaccessible/foo && exit 1 +touch /opt/also-inaccessible/foo && exit 1 +mountpoint /var + +ip link +ip link | grep host-only && exit 1 +ip link | grep host0@ +ip link | grep my-fancy-veth1@ +ip link | grep my-fancy-veth2@ +ip link | grep sd-shared1 +ip link | grep sd-shared2 +ip link | grep mv-sd-macvlan1@ +ip link | grep my-macvlan2@ +ip link | grep iv-sd-ipvlan1@ +ip link | grep my-ipvlan2@ +EOF + timeout 30 systemd-nspawn --directory="$root" + + # And now for stuff that needs to run separately + # + # Note on the condition below: since our container tree is owned by root, + # both "yes" and "identity" private users settings will behave the same + # as PrivateUsers=0:65535, which makes BindUser= fail as the UID already + # exists there, so skip setting it in such case + for private_users in "131072:65536" yes identity pick; do + cat >"/run/systemd/nspawn/$container.nspawn" <<EOF +[Exec] +Hostname=private-users +PrivateUsers=$private_users + +[Files] +PrivateUsersOwnership=auto +BindUser= +$([[ "$private_users" =~ (yes|identity) ]] || echo "BindUser=testuser") +${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"} +EOF + cat "/run/systemd/nspawn/$container.nspawn" + chown -R root:root "$root" + systemd-nspawn --directory="$root" bash -xec '[[ "$(hostname)" == private-users ]]' + done + + rm -fr "$root" "/run/systemd/nspawn/$container.nspawn" +} + +bind_user_cleanup() { + userdel --force --remove nspawn-bind-user-1 + userdel --force --remove nspawn-bind-user-2 +} + +testcase_bind_user() { + local root + + root="$(mktemp -d /var/lib/machines/testsuite-13.bind-user.XXX)" + create_dummy_container "$root" + useradd --create-home --user-group nspawn-bind-user-1 + useradd --create-home --user-group nspawn-bind-user-2 + trap bind_user_cleanup RETURN + touch /home/nspawn-bind-user-1/foo + touch /home/nspawn-bind-user-2/bar + # Add a couple of POSIX ACLs to test the patch-uid stuff + mkdir -p "$root/opt" + setfacl -R -m 'd:u:nspawn-bind-user-1:rwX' -m 'u:nspawn-bind-user-1:rwX' "$root/opt" + setfacl -R -m 'd:g:nspawn-bind-user-1:rwX' -m 'g:nspawn-bind-user-1:rwX' "$root/opt" + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + bash -xec 'test -e /run/host/home/nspawn-bind-user-1/foo' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --private-users-ownership=chown \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'test -e /run/host/home/nspawn-bind-user-1/foo; test -e /run/host/home/nspawn-bind-user-2/bar' + chown -R root:root "$root" + + # User/group name collision + echo "nspawn-bind-user-2:x:1000:1000:nspawn-bind-user-2:/home/nspawn-bind-user-2:/bin/bash" >"$root/etc/passwd" + (! systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + true) + rm -f "$root/etc/passwd" + + echo "nspawn-bind-user-2:x:1000:" >"$root/etc/group" + (! systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + true) + rm -f "$root/etc/group" + + rm -fr "$root" +} + +testcase_bind_tmp_path() { + # https://github.com/systemd/systemd/issues/4789 + local root + + root="$(mktemp -d /var/lib/machines/testsuite-13.bind-tmp-path.XXX)" + create_dummy_container "$root" + : >/tmp/bind + systemd-nspawn --register=no \ + --directory="$root" \ + --bind=/tmp/bind \ + bash -c 'test -e /tmp/bind' + + rm -fr "$root" /tmp/bind +} + +testcase_norbind() { + # https://github.com/systemd/systemd/issues/13170 + local root + + root="$(mktemp -d /var/lib/machines/testsuite-13.norbind-path.XXX)" + mkdir -p /tmp/binddir/subdir + echo -n "outer" >/tmp/binddir/subdir/file + mount -t tmpfs tmpfs /tmp/binddir/subdir + echo -n "inner" >/tmp/binddir/subdir/file + create_dummy_container "$root" + + systemd-nspawn --register=no \ + --directory="$root" \ + --bind=/tmp/binddir:/mnt:norbind \ + bash -c 'CONTENT=$(cat /mnt/subdir/file); if [[ $CONTENT != "outer" ]]; then echo "*** unexpected content: $CONTENT"; exit 1; fi' + + umount /tmp/binddir/subdir + rm -fr "$root" /tmp/binddir/ +} + +rootidmap_cleanup() { + local dir="${1:?}" + + mountpoint -q "$dir/bind" && umount "$dir/bind" + rm -fr "$dir" +} + +testcase_rootidmap() { + local root cmd permissions + local owner=1000 + + root="$(mktemp -d /var/lib/machines/testsuite-13.rootidmap-path.XXX)" + # Create ext4 image, as ext4 supports idmapped-mounts. + mkdir -p /tmp/rootidmap/bind + dd if=/dev/zero of=/tmp/rootidmap/ext4.img bs=4k count=2048 + mkfs.ext4 /tmp/rootidmap/ext4.img + mount /tmp/rootidmap/ext4.img /tmp/rootidmap/bind + trap "rootidmap_cleanup /tmp/rootidmap/" RETURN + + touch /tmp/rootidmap/bind/file + chown -R "$owner:$owner" /tmp/rootidmap/bind + + create_dummy_container "$root" + cmd='PERMISSIONS=$(stat -c "%u:%g" /mnt/file); if [[ $PERMISSIONS != "0:0" ]]; then echo "*** wrong permissions: $PERMISSIONS"; return 1; fi; touch /mnt/other_file' + if ! SYSTEMD_LOG_TARGET=console \ + systemd-nspawn --register=no \ + --directory="$root" \ + --bind=/tmp/rootidmap/bind:/mnt:rootidmap \ + bash -c "$cmd" |& tee nspawn.out; then + if grep -q "Failed to map ids for bind mount.*: Function not implemented" nspawn.out; then + echo "idmapped mounts are not supported, skipping the test..." + return 0 + fi + + return 1 + fi + + permissions=$(stat -c "%u:%g" /tmp/rootidmap/bind/other_file) + if [[ $permissions != "$owner:$owner" ]]; then + echo "*** wrong permissions: $permissions" + [[ "$IS_USERNS_SUPPORTED" == "yes" ]] && return 1 + fi +} + +testcase_notification_socket() { + # https://github.com/systemd/systemd/issues/4944 + local root + local cmd='echo a | nc -U -u -w 1 /run/host/notify' + + root="$(mktemp -d /var/lib/machines/testsuite-13.check_notification_socket.XXX)" + create_dummy_container "$root" + + systemd-nspawn --register=no --directory="$root" bash -x -c "$cmd" + systemd-nspawn --register=no --directory="$root" -U bash -x -c "$cmd" + + rm -fr "$root" +} + +testcase_os_release() { + local root entrypoint os_release_source + + root="$(mktemp -d /var/lib/machines/testsuite-13.os-release.XXX)" + create_dummy_container "$root" + entrypoint="$root/entrypoint.sh" + cat >"$entrypoint" <<\EOF +#!/usr/bin/bash -ex + +. /tmp/os-release +[[ -n "${ID:-}" && "$ID" != "$container_host_id" ]] && exit 1 +[[ -n "${VERSION_ID:-}" && "$VERSION_ID" != "$container_host_version_id" ]] && exit 1 +[[ -n "${BUILD_ID:-}" && "$BUILD_ID" != "$container_host_build_id" ]] && exit 1 +[[ -n "${VARIANT_ID:-}" && "$VARIANT_ID" != "$container_host_variant_id" ]] && exit 1 + +cd /tmp +(cd /run/host && md5sum os-release) | md5sum -c +EOF + chmod +x "$entrypoint" + + os_release_source="/etc/os-release" + if [[ ! -r "$os_release_source" ]]; then + os_release_source="/usr/lib/os-release" + elif [[ -L "$os_release_source" ]]; then + # Ensure that /etc always wins if available + cp --remove-destination -fv /usr/lib/os-release /etc/os-release + echo MARKER=1 >>/etc/os-release + fi + + systemd-nspawn --register=no \ + --directory="$root" \ + --bind="$os_release_source:/tmp/os-release" \ + "${entrypoint##"$root"}" + + if grep -q MARKER /etc/os-release; then + ln -svrf /usr/lib/os-release /etc/os-release + fi + + rm -fr "$root" +} + +testcase_machinectl_bind() { + local service_path service_name root container_name ec + local cmd='for i in $(seq 1 20); do if test -f /tmp/marker; then exit 0; fi; sleep .5; done; exit 1;' + + root="$(mktemp -d /var/lib/machines/testsuite-13.machinectl-bind.XXX)" + create_dummy_container "$root" + container_name="$(basename "$root")" + + service_path="$(mktemp /run/systemd/system/nspawn-machinectl-bind-XXX.service)" + service_name="${service_path##*/}" + cat >"$service_path" <<EOF +[Service] +Type=notify +ExecStart=systemd-nspawn --directory="$root" --notify-ready=no /usr/bin/bash -xec "$cmd" +EOF + + systemctl daemon-reload + systemctl start "$service_name" + touch /tmp/marker + machinectl bind --mkdir "$container_name" /tmp/marker + + timeout 10 bash -c "while [[ '\$(systemctl show -P SubState $service_name)' == running ]]; do sleep .2; done" + ec="$(systemctl show -P ExecMainStatus "$service_name")" + systemctl stop "$service_name" + + rm -fr "$root" "$service_path" + + return "$ec" +} + +testcase_selinux() { + # Basic test coverage to avoid issues like https://github.com/systemd/systemd/issues/19976 + if ! command -v selinuxenabled >/dev/null || ! selinuxenabled; then + echo >&2 "SELinux is not enabled, skipping SELinux-related tests" + return 0 + fi + + local root + + root="$(mktemp -d /var/lib/machines/testsuite-13.selinux.XXX)" + create_dummy_container "$root" + chcon -R -t container_t "$root" + + systemd-nspawn --register=no \ + --boot \ + --directory="$root" \ + --selinux-apifs-context=system_u:object_r:container_file_t:s0:c0,c1 \ + --selinux-context=system_u:system_r:container_t:s0:c0,c1 + + rm -fr "$root" +} + +testcase_ephemeral_config() { + # https://github.com/systemd/systemd/issues/13297 + local root container_name + + root="$(mktemp -d /var/lib/machines/testsuite-13.ephemeral-config.XXX)" + create_dummy_container "$root" + container_name="$(basename "$root")" + + mkdir -p /run/systemd/nspawn/ + rm -f "/etc/systemd/nspawn/$container_name.nspawn" + cat >"/run/systemd/nspawn/$container_name.nspawn" <<EOF +[Files] +${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"} +BindReadOnly=/tmp/ephemeral-config +EOF + touch /tmp/ephemeral-config + + systemd-nspawn --register=no \ + --directory="$root" \ + --ephemeral \ + bash -x -c "test -f /tmp/ephemeral-config" + + systemd-nspawn --register=no \ + --directory="$root" \ + --ephemeral \ + --machine=foobar \ + bash -x -c "! test -f /tmp/ephemeral-config" + + rm -fr "$root" "/run/systemd/nspawn/$container_name.nspawn" +} + +matrix_run_one() { + local cgroupsv2="${1:?}" + local use_cgns="${2:?}" + local api_vfs_writable="${3:?}" + local root + + if [[ "$cgroupsv2" == "yes" && "$IS_CGROUPSV2_SUPPORTED" == "no" ]]; then + echo >&2 "Unified cgroup hierarchy is not supported, skipping..." + return 0 + fi + + if [[ "$use_cgns" == "yes" && "$IS_CGNS_SUPPORTED" == "no" ]]; then + echo >&2 "CGroup namespaces are not supported, skipping..." + return 0 + fi + + root="$(mktemp -d "/var/lib/machines/testsuite-13.unified-$1-cgns-$2-api-vfs-writable-$3.XXX")" + create_dummy_container "$root" + + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --boot + + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --private-network \ + --boot + + if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --private-users=pick \ + --boot; then + [[ "$IS_USERNS_SUPPORTED" == "yes" && "$api_vfs_writable" == "network" ]] && return 1 + else + [[ "$IS_USERNS_SUPPORTED" == "no" && "$api_vfs_writable" = "network" ]] && return 1 + fi + + if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --private-network \ + --private-users=pick \ + --boot; then + [[ "$IS_USERNS_SUPPORTED" == "yes" && "$api_vfs_writable" == "yes" ]] && return 1 + else + [[ "$IS_USERNS_SUPPORTED" == "no" && "$api_vfs_writable" = "yes" ]] && return 1 + fi + + local netns_opt="--network-namespace-path=/proc/self/ns/net" + local net_opt + local net_opts=( + "--network-bridge=lo" + "--network-interface=lo" + "--network-ipvlan=lo" + "--network-macvlan=lo" + "--network-veth" + "--network-veth-extra=lo" + "--network-zone=zone" + ) + + # --network-namespace-path and network-related options cannot be used together + for net_opt in "${net_opts[@]}"; do + echo "$netns_opt in combination with $net_opt should fail" + if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --boot \ + "$netns_opt" \ + "$net_opt"; then + echo >&2 "unexpected pass" + return 1 + fi + done + + # allow combination of --network-namespace-path and --private-network + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --boot \ + --private-network \ + "$netns_opt" + + # test --network-namespace-path works with a network namespace created by "ip netns" + ip netns add nspawn_test + netns_opt="--network-namespace-path=/run/netns/nspawn_test" + SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ + systemd-nspawn --register=no \ + --directory="$root" \ + --network-namespace-path=/run/netns/nspawn_test \ + ip a | grep -v -E '^1: lo.*UP' + ip netns del nspawn_test + + rm -fr "$root" + + return 0 +} + +testcase_check_os_release() { + # https://github.com/systemd/systemd/issues/29185 + local base common_opts root + + base="$(mktemp -d /var/lib/machines/testsuite-13.check_os_release_base.XXX)" + root="$(mktemp -d /var/lib/machines/testsuite-13.check_os_release.XXX)" + create_dummy_container "$base" + cp -d "$base"/{bin,sbin,lib,lib64} "$root/" + common_opts=( + --boot + --register=no + --directory="$root" + --bind-ro="$base/usr:/usr" + ) + + # Empty /etc/ & /usr/ + (! systemd-nspawn "${common_opts[@]}") + (! SYSTEMD_NSPAWN_CHECK_OS_RELEASE=1 systemd-nspawn "${common_opts[@]}") + (! SYSTEMD_NSPAWN_CHECK_OS_RELEASE=foo systemd-nspawn "${common_opts[@]}") + SYSTEMD_NSPAWN_CHECK_OS_RELEASE=0 systemd-nspawn "${common_opts[@]}" + + # Empty /usr/ + a broken /etc/os-release -> /usr/os-release symlink + ln -svrf "$root/etc/os-release" "$root/usr/os-release" + (! systemd-nspawn "${common_opts[@]}") + (! SYSTEMD_NSPAWN_CHECK_OS_RELEASE=1 systemd-nspawn "${common_opts[@]}") + SYSTEMD_NSPAWN_CHECK_OS_RELEASE=0 systemd-nspawn "${common_opts[@]}" + + rm -fr "$root" "$base" +} + +run_testcases + +for api_vfs_writable in yes no network; do + matrix_run_one no no $api_vfs_writable + matrix_run_one yes no $api_vfs_writable + matrix_run_one no yes $api_vfs_writable + matrix_run_one yes yes $api_vfs_writable +done diff --git a/test/units/testsuite-13.nss-mymachines.sh b/test/units/testsuite-13.nss-mymachines.sh new file mode 100755 index 0000000..b566c73 --- /dev/null +++ b/test/units/testsuite-13.nss-mymachines.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +at_exit() { + set +e + + machinectl kill --signal=KILL nss-mymachines-{noip,singleip,manyips} + mountpoint -q /var/lib/machines && timeout 10 sh -c "until umount /var/lib/machines; do sleep .5; done" + rm -f /run/systemd/nspawn/*.nspawn +} + +trap at_exit EXIT + +mkdir -p /var/lib/machines +mount -t tmpfs tmpfs /var/lib/machines + +# Create a bunch of containers that: +# 1) Have no IP addresses assigned +create_dummy_container /var/lib/machines/nss-mymachines-noip +cat >/var/lib/machines/nss-mymachines-noip/sbin/init <<\EOF +#!/usr/bin/bash -ex + +ip addr show dev ve-noip +touch /initialized +sleep infinity & +# Run the sleep command asynchronously, so bash is able to process signals +while :; do + wait || : +done +EOF +# 2) Have one IP address assigned (IPv4 only) +create_dummy_container /var/lib/machines/nss-mymachines-singleip +cat >/var/lib/machines/nss-mymachines-singleip/sbin/init <<\EOF +#!/usr/bin/bash -ex + +ip addr add 10.1.0.2/24 dev ve-singleip +ip addr show dev ve-singleip +touch /initialized +sleep infinity & +while :; do + wait || : +done +EOF +# 3) Have bunch of IP addresses assigned (both IPv4 and IPv6) +create_dummy_container /var/lib/machines/nss-mymachines-manyips +cat >/var/lib/machines/nss-mymachines-manyips/sbin/init <<\EOF +#!/usr/bin/bash -ex + +ip addr add 10.2.0.2/24 dev ve-manyips +for i in {100..120}; do + ip addr add 10.2.0.$i/24 dev ve-manyips +done +ip addr add fd00:dead:beef:cafe::2/64 dev ve-manyips +ip addr show dev ve-manyips +touch /initialized +sleep infinity +while :; do + wait || : +done +EOF +# Create the respective .nspawn config files +mkdir -p /run/systemd/nspawn +for container in noip singleip manyips; do + cat >"/run/systemd/nspawn/nss-mymachines-$container.nspawn" <<EOF +[Exec] +Boot=yes + +[Network] +VirtualEthernetExtra=ve-$container +EOF +done + +# Start the containers and wait until all of them are initialized +machinectl start nss-mymachines-{noip,singleip,manyips} +for container in nss-mymachines-{noip,singleip,manyips}; do + timeout 30 bash -xec "while [[ ! -e /var/lib/machines/$container/initialized ]]; do sleep .5; done" +done + +# We need to configure the dummy interfaces on the "outside" as well for `getent {ahosts4,ahosts6}` to work +# properly. This is caused by getaddrinfo() calling _check_pf() that iterates through all interfaces and +# notes if any of them has an IPv4/IPv6 - this is then used together with AF_INET/AF_INET6 to determine if we +# can ever return a valid answer, and if we configured the container interfaces only in the container, we +# would have no valid IPv4/IPv6 on the "outside" (as we don't set up any other netdev) which would make +# getaddrinfo() return EAI_NONAME without ever asking nss-mymachines. +ip addr add 10.1.0.1/24 dev ve-singleip +ip addr add 10.2.0.1/24 dev ve-manyips +ip addr add fd00:dead:beef:cafe::1/64 dev ve-manyips + +getent hosts -s mymachines +getent ahosts -s mymachines + +# And finally check if we can resolve the containers via nss-mymachines +for database in hosts ahosts{,v4,v6}; do + (! getent "$database" -s mymachines nss-mymachines-noip) +done + +run_and_grep "^10\.1\.0\.2\s+nss-mymachines-singleip$" getent hosts -s mymachines nss-mymachines-singleip +run_and_grep "^10\.1\.0\.2\s+STREAM" getent ahosts -s mymachines nss-mymachines-singleip +run_and_grep "^10\.1\.0\.2\s+STREAM" getent ahostsv4 -s mymachines nss-mymachines-singleip +run_and_grep "^::ffff:10\.1\.0\.2\s+STREAM" getent ahostsv6 -s mymachines nss-mymachines-singleip + +run_and_grep "^fd00:dead:beef:cafe::2\s+nss-mymachines-manyips$" getent hosts -s mymachines nss-mymachines-manyips +run_and_grep "^fd00:dead:beef:cafe::2\s+STREAM" getent ahosts -s mymachines nss-mymachines-manyips +run_and_grep "^10\.2\.0\.2\s+STREAM" getent ahosts -s mymachines nss-mymachines-manyips +for i in {100..120}; do + run_and_grep "^10\.2\.0\.$i\s+STREAM" getent ahosts -s mymachines nss-mymachines-manyips + run_and_grep "^10\.2\.0\.$i\s+STREAM" getent ahostsv4 -s mymachines nss-mymachines-manyips +done +run_and_grep "^fd00:dead:beef:cafe::2\s+STREAM" getent ahostsv6 -s mymachines nss-mymachines-manyips +(! run_and_grep "^fd00:" getent ahostsv4 -s mymachines nss-mymachines-manyips) +(! run_and_grep "^10\.2:" getent ahostsv6 -s mymachines nss-mymachines-manyips) + +# Multiple machines at once +run_and_grep "^10\.1\.0\.2\s+nss-mymachines-singleip$" getent hosts -s mymachines nss-mymachines-{singleip,manyips} +run_and_grep "^fd00:dead:beef:cafe::2\s+nss-mymachines-manyips$" getent hosts -s mymachines nss-mymachines-{singleip,manyips} +run_and_grep "^10\.1\.0\.2\s+STREAM" getent ahosts -s mymachines nss-mymachines-{singleip,manyips} +run_and_grep "^10\.2\.0\.2\s+STREAM" getent ahosts -s mymachines nss-mymachines-{singleip,manyips} +run_and_grep "^fd00:dead:beef:cafe::2\s+STREAM" getent ahosts -s mymachines nss-mymachines-{singleip,manyips} + +for database in hosts ahosts ahostsv4 ahostsv6; do + (! getent "$database" -s mymachines foo-bar-baz) +done + +# getgrid(), getgrnam(), getpwuid(), and getpwnam() are explicitly handled by nss-mymachines, so probe them +# as well +(! getent group -s mymachines foo 11) +(! getent passwd -s mymachines foo 11) + +machinectl stop nss-mymachines-{noip,singleip,manyips} diff --git a/test/units/testsuite-13.service b/test/units/testsuite-13.service new file mode 100644 index 0000000..95c9a02 --- /dev/null +++ b/test/units/testsuite-13.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-13-NSPAWN + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-13.sh b/test/units/testsuite-13.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-13.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-15.service b/test/units/testsuite-15.service new file mode 100644 index 0000000..10af93f --- /dev/null +++ b/test/units/testsuite-15.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-15-DROPIN + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-15.sh b/test/units/testsuite-15.sh new file mode 100755 index 0000000..e790b37 --- /dev/null +++ b/test/units/testsuite-15.sh @@ -0,0 +1,711 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +clear_unit() { + local unit_name="${1:?}" + local base suffix + + systemctl stop "$unit_name" 2>/dev/null || : + rm -f /{etc,run,usr/lib}/systemd/system/"$unit_name" + rm -fr /{etc,run,usr/lib}/systemd/system/"$unit_name".d + rm -fr /{etc,run,usr/lib}/systemd/system/"$unit_name".{wants,requires} + if [[ $unit_name == *@* ]]; then + base="${unit_name%@*}" + suffix="${unit_name##*.}" + systemctl stop "$base@"*."$suffix" 2>/dev/null || : + rm -f /{etc,run,usr/lib}/systemd/system/"$base@"*."$suffix" + rm -fr /{etc,run,usr/lib}/systemd/system/"$base@"*."$suffix".d + rm -fr /{etc,run,usr/lib}/systemd/system/"$base@"*."$suffix".{wants,requires} + fi +} + +clear_units() { + for u in "$@"; do + clear_unit "$u" + done + systemctl daemon-reload +} + +create_service() { + local service_name="${1:?}" + clear_units "${service_name}".service + + cat >/etc/systemd/system/"$service_name".service <<EOF +[Unit] +Description=$service_name unit + +[Service] +ExecStart=sleep 100000 +EOF + mkdir -p /{etc,run,usr/lib}/systemd/system/"$service_name".service.{d,wants,requires} +} + +create_services() { + for u in "$@"; do + create_service "$u" + done +} + +check_ok() { + x="$(systemctl show --value -p "${2:?}" "${1:?}")" + case "$x" in + *${3:?}*) return 0 ;; + *) return 1 ;; + esac +} + +check_ko() { + ! check_ok "$@" +} + +testcase_basic_dropins() { + echo "Testing basic dropins..." + + echo "*** test a wants b wants c" + create_services test15-a test15-b test15-c + ln -s ../test15-b.service /etc/systemd/system/test15-a.service.wants/ + ln -s ../test15-c.service /etc/systemd/system/test15-b.service.wants/ + check_ok test15-a Wants test15-b.service + check_ok test15-b Wants test15-c.service + + echo "*** test a wants,requires b" + create_services test15-a test15-b test15-c + ln -s ../test15-b.service /etc/systemd/system/test15-a.service.wants/ + ln -s ../test15-b.service /etc/systemd/system/test15-a.service.requires/ + check_ok test15-a Wants test15-b.service + check_ok test15-a Requires test15-b.service + + echo "*** test a wants nonexistent" + create_service test15-a + ln -s ../nonexistent.service /etc/systemd/system/test15-a.service.wants/ + check_ok test15-a Wants nonexistent.service + systemctl start test15-a + systemctl stop test15-a + + echo "*** test a requires nonexistent" + ln -sf ../nonexistent.service /etc/systemd/system/test15-a.service.requires/ + systemctl daemon-reload + check_ok test15-a Requires nonexistent.service + + # 'b' is already loaded when 'c' pulls it in via a dropin. + echo "*** test a,c require b" + create_services test15-a test15-b test15-c + ln -sf ../test15-b.service /etc/systemd/system/test15-a.service.requires/ + ln -sf ../test15-b.service /etc/systemd/system/test15-c.service.requires/ + systemctl start test15-a + check_ok test15-c Requires test15-b.service + systemctl stop test15-a test15-b + + # 'b' is already loaded when 'c' pulls it in via an alias dropin. + echo "*** test a wants alias" + create_services test15-a test15-b test15-c + ln -sf test15-c.service /etc/systemd/system/test15-c1.service + ln -sf ../test15-c.service /etc/systemd/system/test15-a.service.wants/ + ln -sf ../test15-c1.service /etc/systemd/system/test15-b.service.wants/ + systemctl start test15-a + check_ok test15-a Wants test15-c.service + check_ok test15-b Wants test15-c.service + systemctl stop test15-a test15-c + + echo "*** test service.d/ top level drop-in" + create_services test15-a test15-b + check_ko test15-a ExecCondition "/bin/echo a" + check_ko test15-b ExecCondition "/bin/echo b" + mkdir -p /run/systemd/system/service.d + cat >/run/systemd/system/service.d/override.conf <<EOF +[Service] +ExecCondition=/bin/echo %n +EOF + systemctl daemon-reload + check_ok test15-a ExecCondition "/bin/echo test15-a" + check_ok test15-b ExecCondition "/bin/echo test15-b" + rm -rf /run/systemd/system/service.d + + clear_units test15-{a,b,c,c1}.service +} + +testcase_linked_units() { + echo "Testing linked units..." + echo "*** test linked unit (same basename)" + + create_service test15-a + mv /etc/systemd/system/test15-a.service / + ln -s /test15-a.service /etc/systemd/system/ + ln -s test15-a.service /etc/systemd/system/test15-b.service + + check_ok test15-a Names test15-a.service + check_ok test15-a Names test15-b.service + + echo "*** test linked unit (cross basename)" + + mv /test15-a.service /test15-a@.scope + ln -fs /test15-a@.scope /etc/systemd/system/test15-a.service + systemctl daemon-reload + + check_ok test15-a Names test15-a.service + check_ok test15-a Names test15-b.service + check_ko test15-a Names test15-a@ # test15-a@.scope is the symlink target. + # Make sure it is completely ignored. + + rm /test15-a@.scope + clear_units test15-{a,b}.service +} + +testcase_template_alias() { + echo "Testing instance alias..." + echo "*** forward" + + create_service test15-a@ + ln -s test15-a@inst.service /etc/systemd/system/test15-b@inst.service # alias + + check_ok test15-a@inst Names test15-a@inst.service + check_ok test15-a@inst Names test15-b@inst.service + + check_ok test15-a@other Names test15-a@other.service + check_ko test15-a@other Names test15-b@other.service + + echo "*** reverse" + + systemctl daemon-reload + + check_ok test15-b@inst Names test15-a@inst.service + check_ok test15-b@inst Names test15-b@inst.service + + check_ko test15-b@other Names test15-a@other.service + check_ok test15-b@other Names test15-b@other.service + + clear_units test15-{a,b}@.service +} + +testcase_hierarchical_service_dropins() { + echo "Testing hierarchical service dropins..." + echo "*** test service.d/ top level drop-in" + create_services a-b-c + check_ko a-b-c ExecCondition "echo service.d" + check_ko a-b-c ExecCondition "echo a-.service.d" + check_ko a-b-c ExecCondition "echo a-b-.service.d" + check_ko a-b-c ExecCondition "echo a-b-c.service.d" + + for dropin in service.d a-.service.d a-b-.service.d a-b-c.service.d; do + mkdir -p "/run/systemd/system/$dropin" + cat >"/run/systemd/system/$dropin/override.conf" <<EOF +[Service] +ExecCondition=echo $dropin +EOF + systemctl daemon-reload + check_ok a-b-c ExecCondition "echo $dropin" + + # Check that we can start a transient service in presence of the drop-ins + systemd-run -u a-b-c2.service -p Description='sleepy' sleep infinity + + # The transient setting replaces the default + check_ok a-b-c2.service Description "sleepy" + + # The override takes precedence for ExecCondition + # (except the last iteration when it only applies to the other service) + if [ "$dropin" != "a-b-c.service.d" ]; then + check_ok a-b-c2.service ExecCondition "echo $dropin" + fi + + # Check that things are the same after a reload + systemctl daemon-reload + check_ok a-b-c2.service Description "sleepy" + if [ "$dropin" != "a-b-c.service.d" ]; then + check_ok a-b-c2.service ExecCondition "echo $dropin" + fi + + systemctl stop a-b-c2.service + done + for dropin in service.d a-.service.d a-b-.service.d a-b-c.service.d; do + rm -rf "/run/systemd/system/$dropin" + done + + clear_units a-b-c.service +} + +testcase_hierarchical_slice_dropins() { + echo "Testing hierarchical slice dropins..." + echo "*** test slice.d/ top level drop-in" + # Slice units don't even need a fragment, so we test the defaults here + check_ok a-b-c.slice Description "Slice /a/b/c" + check_ok a-b-c.slice MemoryMax "infinity" + + # Test drop-ins + for dropin in slice.d a-.slice.d a-b-.slice.d a-b-c.slice.d; do + mkdir -p "/run/systemd/system/$dropin" + cat >"/run/systemd/system/$dropin/override.conf" <<EOF +[Slice] +MemoryMax=1000000000 +EOF + systemctl daemon-reload + check_ok a-b-c.slice MemoryMax "1000000000" + + busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + StartTransientUnit 'ssa(sv)a(sa(sv))' \ + 'a-b-c.slice' 'replace' \ + 2 \ + 'Description' s 'slice too' \ + 'MemoryMax' t 1000000002 \ + 0 + + # The override takes precedence for MemoryMax + check_ok a-b-c.slice MemoryMax "1000000000" + # The transient setting replaces the default + check_ok a-b-c.slice Description "slice too" + + # Check that things are the same after a reload + systemctl daemon-reload + check_ok a-b-c.slice MemoryMax "1000000000" + check_ok a-b-c.slice Description "slice too" + + busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + StopUnit 'ss' \ + 'a-b-c.slice' 'replace' + + rm -f "/run/systemd/system/$dropin/override.conf" + done + + # Test unit with a fragment + cat >/run/systemd/system/a-b-c.slice <<EOF +[Slice] +MemoryMax=1000000001 +EOF + systemctl daemon-reload + check_ok a-b-c.slice MemoryMax "1000000001" + + clear_units a-b-c.slice +} + +testcase_transient_service_dropins() { + echo "Testing dropins for a transient service..." + echo "*** test transient service drop-ins" + + mkdir -p /etc/systemd/system/service.d + mkdir -p /etc/systemd/system/a-.service.d + mkdir -p /etc/systemd/system/a-b-.service.d + mkdir -p /etc/systemd/system/a-b-c.service.d + + echo -e '[Service]\nStandardInputText=aaa' >/etc/systemd/system/service.d/drop1.conf + echo -e '[Service]\nStandardInputText=bbb' >/etc/systemd/system/a-.service.d/drop2.conf + echo -e '[Service]\nStandardInputText=ccc' >/etc/systemd/system/a-b-.service.d/drop3.conf + echo -e '[Service]\nStandardInputText=ddd' >/etc/systemd/system/a-b-c.service.d/drop4.conf + + # There's no fragment yet, so this fails + systemctl cat a-b-c.service && exit 1 + + # xxx → eHh4Cg== + systemd-run -u a-b-c.service -p StandardInputData=eHh4Cg== sleep infinity + + data=$(systemctl show -P StandardInputData a-b-c.service) + # xxx\naaa\n\bbb\nccc\nddd\n → eHh4… + test "$data" = "eHh4CmFhYQpiYmIKY2NjCmRkZAo=" + + # Do a reload and check again + systemctl daemon-reload + data=$(systemctl show -P StandardInputData a-b-c.service) + test "$data" = "eHh4CmFhYQpiYmIKY2NjCmRkZAo=" + + clear_units a-b-c.service + rm /etc/systemd/system/service.d/drop1.conf \ + /etc/systemd/system/a-.service.d/drop2.conf \ + /etc/systemd/system/a-b-.service.d/drop3.conf +} + +testcase_transient_slice_dropins() { + echo "Testing dropins for a transient slice..." + echo "*** test transient slice drop-ins" + + # FIXME: implement reloading of individual units. + # + # The settings here are loaded twice. For most settings it doesn't matter, + # but Documentation is not deduplicated, so we current get repeated entried + # which is a bug. + + mkdir -p /etc/systemd/system/slice.d + mkdir -p /etc/systemd/system/a-.slice.d + mkdir -p /etc/systemd/system/a-b-.slice.d + mkdir -p /etc/systemd/system/a-b-c.slice.d + + echo -e '[Unit]\nDocumentation=man:drop1' >/etc/systemd/system/slice.d/drop1.conf + echo -e '[Unit]\nDocumentation=man:drop2' >/etc/systemd/system/a-.slice.d/drop2.conf + echo -e '[Unit]\nDocumentation=man:drop3' >/etc/systemd/system/a-b-.slice.d/drop3.conf + echo -e '[Unit]\nDocumentation=man:drop4' >/etc/systemd/system/a-b-c.slice.d/drop4.conf + + # Invoke daemon-reload to make sure that the call below doesn't fail + systemctl daemon-reload + + # No fragment is required, so this works + systemctl cat a-b-c.slice + + busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + StartTransientUnit 'ssa(sv)a(sa(sv))' \ + 'a-b-c.slice' 'replace' \ + 1 \ + 'Documentation' as 1 'man:drop5' \ + 0 + + data=$(systemctl show -P Documentation a-b-c.slice) + test "$data" = "man:drop1 man:drop2 man:drop3 man:drop4 man:drop5 man:drop1 man:drop2 man:drop3 man:drop4" + + # Do a reload and check again + systemctl daemon-reload + data=$(systemctl show -P Documentation a-b-c.slice) + test "$data" = "man:drop5 man:drop1 man:drop2 man:drop3 man:drop4" + + clear_units a-b-c.slice + rm /etc/systemd/system/slice.d/drop1.conf \ + /etc/systemd/system/a-.slice.d/drop2.conf \ + /etc/systemd/system/a-b-.slice.d/drop3.conf +} + +testcase_template_dropins() { + echo "Testing template dropins..." + + create_services foo bar@ yup@ + + # Declare some deps to check if the body was loaded + cat >>/etc/systemd/system/bar@.service <<EOF +[Unit] +After=bar-template-after.device +EOF + + cat >>/etc/systemd/system/yup@.service <<EOF +[Unit] +After=yup-template-after.device +EOF + + ln -s /etc/systemd/system/bar@.service /etc/systemd/system/foo.service.wants/bar@1.service + check_ok foo Wants bar@1.service + + echo "*** test bar-alias@.service→bar@.service, but instance symlinks point to yup@.service ***" + ln -s bar@.service /etc/systemd/system/bar-alias@.service + ln -s bar@1.service /etc/systemd/system/bar-alias@1.service + ln -s yup@.service /etc/systemd/system/bar-alias@2.service + ln -s yup@3.service /etc/systemd/system/bar-alias@3.service + + # create some dropin deps + mkdir -p /etc/systemd/system/bar@{,0,1,2,3}.service.requires/ + mkdir -p /etc/systemd/system/yup@{,0,1,2,3}.service.requires/ + mkdir -p /etc/systemd/system/bar-alias@{,0,1,2,3}.service.requires/ + + ln -s ../bar-template-requires.device /etc/systemd/system/bar@.service.requires/ + ln -s ../bar-0-requires.device /etc/systemd/system/bar@0.service.requires/ + ln -s ../bar-1-requires.device /etc/systemd/system/bar@1.service.requires/ + ln -s ../bar-2-requires.device /etc/systemd/system/bar@2.service.requires/ + ln -s ../bar-3-requires.device /etc/systemd/system/bar@3.service.requires/ + + ln -s ../yup-template-requires.device /etc/systemd/system/yup@.service.requires/ + ln -s ../yup-0-requires.device /etc/systemd/system/yup@0.service.requires/ + ln -s ../yup-1-requires.device /etc/systemd/system/yup@1.service.requires/ + ln -s ../yup-2-requires.device /etc/systemd/system/yup@2.service.requires/ + ln -s ../yup-3-requires.device /etc/systemd/system/yup@3.service.requires/ + + ln -s ../bar-alias-template-requires.device /etc/systemd/system/bar-alias@.service.requires/ + ln -s ../bar-alias-0-requires.device /etc/systemd/system/bar-alias@0.service.requires/ + ln -s ../bar-alias-1-requires.device /etc/systemd/system/bar-alias@1.service.requires/ + ln -s ../bar-alias-2-requires.device /etc/systemd/system/bar-alias@2.service.requires/ + ln -s ../bar-alias-3-requires.device /etc/systemd/system/bar-alias@3.service.requires/ + + systemctl daemon-reload + + echo '*** bar@0 is aliased by bar-alias@0 ***' + systemctl show -p Names,Requires bar@0 + systemctl show -p Names,Requires bar-alias@0 + check_ok bar@0 Names bar@0 + check_ok bar@0 Names bar-alias@0 + + check_ok bar@0 After bar-template-after.device + + check_ok bar@0 Requires bar-0-requires.device + check_ok bar@0 Requires bar-alias-0-requires.device + check_ok bar@0 Requires bar-template-requires.device + check_ok bar@0 Requires bar-alias-template-requires.device + check_ko bar@0 Requires yup-template-requires.device + + check_ok bar-alias@0 After bar-template-after.device + + check_ok bar-alias@0 Requires bar-0-requires.device + check_ok bar-alias@0 Requires bar-alias-0-requires.device + check_ok bar-alias@0 Requires bar-template-requires.device + check_ok bar-alias@0 Requires bar-alias-template-requires.device + check_ko bar-alias@0 Requires yup-template-requires.device + check_ko bar-alias@0 Requires yup-0-requires.device + + echo '*** bar@1 is aliased by bar-alias@1 ***' + systemctl show -p Names,Requires bar@1 + systemctl show -p Names,Requires bar-alias@1 + check_ok bar@1 Names bar@1 + check_ok bar@1 Names bar-alias@1 + + check_ok bar@1 After bar-template-after.device + + check_ok bar@1 Requires bar-1-requires.device + check_ok bar@1 Requires bar-alias-1-requires.device + check_ok bar@1 Requires bar-template-requires.device + # See https://github.com/systemd/systemd/pull/13119#discussion_r308145418 + check_ok bar@1 Requires bar-alias-template-requires.device + check_ko bar@1 Requires yup-template-requires.device + check_ko bar@1 Requires yup-1-requires.device + + check_ok bar-alias@1 After bar-template-after.device + + check_ok bar-alias@1 Requires bar-1-requires.device + check_ok bar-alias@1 Requires bar-alias-1-requires.device + check_ok bar-alias@1 Requires bar-template-requires.device + check_ok bar-alias@1 Requires bar-alias-template-requires.device + check_ko bar-alias@1 Requires yup-template-requires.device + check_ko bar-alias@1 Requires yup-1-requires.device + + echo '*** bar-alias@2 aliases yup@2, bar@2 is independent ***' + systemctl show -p Names,Requires bar@2 + systemctl show -p Names,Requires bar-alias@2 + check_ok bar@2 Names bar@2 + check_ko bar@2 Names bar-alias@2 + + check_ok bar@2 After bar-template-after.device + + check_ok bar@2 Requires bar-2-requires.device + check_ko bar@2 Requires bar-alias-2-requires.device + check_ok bar@2 Requires bar-template-requires.device + check_ko bar@2 Requires bar-alias-template-requires.device + check_ko bar@2 Requires yup-template-requires.device + check_ko bar@2 Requires yup-2-requires.device + + check_ko bar-alias@2 After bar-template-after.device + + check_ko bar-alias@2 Requires bar-2-requires.device + check_ok bar-alias@2 Requires bar-alias-2-requires.device + check_ko bar-alias@2 Requires bar-template-requires.device + check_ok bar-alias@2 Requires bar-alias-template-requires.device + check_ok bar-alias@2 Requires yup-template-requires.device + check_ok bar-alias@2 Requires yup-2-requires.device + + echo '*** bar-alias@3 aliases yup@3, bar@3 is independent ***' + systemctl show -p Names,Requires bar@3 + systemctl show -p Names,Requires bar-alias@3 + check_ok bar@3 Names bar@3 + check_ko bar@3 Names bar-alias@3 + + check_ok bar@3 After bar-template-after.device + + check_ok bar@3 Requires bar-3-requires.device + check_ko bar@3 Requires bar-alias-3-requires.device + check_ok bar@3 Requires bar-template-requires.device + check_ko bar@3 Requires bar-alias-template-requires.device + check_ko bar@3 Requires yup-template-requires.device + check_ko bar@3 Requires yup-3-requires.device + + check_ko bar-alias@3 After bar-template-after.device + + check_ko bar-alias@3 Requires bar-3-requires.device + check_ok bar-alias@3 Requires bar-alias-3-requires.device + check_ko bar-alias@3 Requires bar-template-requires.device + check_ok bar-alias@3 Requires bar-alias-template-requires.device + check_ok bar-alias@3 Requires yup-template-requires.device + check_ok bar-alias@3 Requires yup-3-requires.device + + clear_units foo.service {bar,yup,bar-alias}@{,1,2,3}.service +} + +testcase_alias_dropins() { + echo "Testing alias dropins..." + + echo "*** test a wants b1 alias of b" + create_services test15-a test15-b + ln -sf test15-b.service /etc/systemd/system/test15-b1.service + ln -sf ../test15-b1.service /etc/systemd/system/test15-a.service.wants/ + check_ok test15-a Wants test15-b.service + systemctl start test15-a + systemctl --quiet is-active test15-b + systemctl stop test15-a test15-b + rm /etc/systemd/system/test15-b1.service + clear_units test15-{a,b}.service + + # Check that dependencies don't vary. + echo "*** test 2" + create_services test15-a test15-x test15-y + mkdir -p /etc/systemd/system/test15-a1.service.wants/ + ln -sf test15-a.service /etc/systemd/system/test15-a1.service + ln -sf ../test15-x.service /etc/systemd/system/test15-a.service.wants/ + ln -sf ../test15-y.service /etc/systemd/system/test15-a1.service.wants/ + check_ok test15-a1 Wants test15-x.service # see [1] + check_ok test15-a1 Wants test15-y.service + systemctl start test15-a + check_ok test15-a1 Wants test15-x.service # see [2] + check_ok test15-a1 Wants test15-y.service + systemctl stop test15-a test15-x test15-y + rm /etc/systemd/system/test15-a1.service + + clear_units test15-{a,x,y}.service +} + +testcase_masked_dropins() { + echo "Testing masked dropins..." + + create_services test15-a test15-b + + # 'b' is masked for both deps + echo "*** test a wants,requires b is masked" + ln -sf /dev/null /etc/systemd/system/test15-a.service.wants/test15-b.service + ln -sf /dev/null /etc/systemd/system/test15-a.service.requires/test15-b.service + check_ko test15-a Wants test15-b.service + check_ko test15-a Requires test15-b.service + + # 'a' wants 'b' and 'b' is masked at a lower level + echo "*** test a wants b, mask override" + ln -sf ../test15-b.service /etc/systemd/system/test15-a.service.wants/test15-b.service + ln -sf /dev/null /usr/lib/systemd/system/test15-a.service.wants/test15-b.service + check_ok test15-a Wants test15-b.service + + # 'a' wants 'b' and 'b' is masked at a higher level + echo "*** test a wants b, mask" + ln -sf /dev/null /etc/systemd/system/test15-a.service.wants/test15-b.service + ln -sf ../test15-b.service /usr/lib/systemd/system/test15-a.service.wants/test15-b.service + check_ko test15-a Wants test15-b.service + + # 'a' is masked but has an override config file + echo "*** test a is masked but has an override" + create_services test15-a test15-b + ln -sf /dev/null /etc/systemd/system/test15-a.service + cat >/usr/lib/systemd/system/test15-a.service.d/override.conf <<EOF +[Unit] +After=test15-b.service +EOF + check_ok test15-a UnitFileState masked + + # 'b1' is an alias for 'b': masking 'b' dep should not influence 'b1' dep + echo "*** test a wants b, b1, and one is masked" + create_services test15-a test15-b + ln -sf test15-b.service /etc/systemd/system/test15-b1.service + ln -sf /dev/null /etc/systemd/system/test15-a.service.wants/test15-b.service + ln -sf ../test15-b1.service /usr/lib/systemd/system/test15-a.service.wants/test15-b1.service + systemctl cat test15-a + systemctl show -p Wants,Requires test15-a + systemctl cat test15-b1 + systemctl show -p Wants,Requires test15-b1 + check_ok test15-a Wants test15-b.service + check_ko test15-a Wants test15-b1.service # the alias does not show up in the list of units + rm /etc/systemd/system/test15-b1.service + + # 'b1' is an alias for 'b': masking 'b1' should not influence 'b' dep + echo "*** test a wants b, alias dep is masked" + create_services test15-a test15-b + ln -sf test15-b.service /etc/systemd/system/test15-b1.service + ln -sf /dev/null /etc/systemd/system/test15-a.service.wants/test15-b1.service + ln -sf ../test15-b.service /usr/lib/systemd/system/test15-a.service.wants/test15-b.service + check_ok test15-a Wants test15-b.service + check_ko test15-a Wants test15-b1.service # the alias does not show up in the list of units + rm /etc/systemd/system/test15-b1.service + + # 'a' has Wants=b.service but also has a masking + # dropin 'b': 'b' should still be pulled in. + echo "*** test a wants b both ways" + create_services test15-a test15-b + ln -sf /dev/null /etc/systemd/system/test15-a.service.wants/test15-b.service + cat >/usr/lib/systemd/system/test15-a.service.d/wants-b.conf <<EOF +[Unit] +Wants=test15-b.service +EOF + check_ok test15-a Wants test15-b.service + + # mask a dropin that points to an nonexistent unit. + echo "*** test a wants nonexistent is masked" + create_services test15-a + ln -sf /dev/null /etc/systemd/system/test15-a.service.requires/nonexistent.service + ln -sf ../nonexistent.service /usr/lib/systemd/system/test15-a.service.requires/ + check_ko test15-a Requires nonexistent.service + + # 'b' is already loaded when 'c' pulls it in via a dropin but 'b' is + # masked at a higher level. + echo "*** test a wants b is masked" + create_services test15-a test15-b test15-c + ln -sf ../test15-b.service /etc/systemd/system/test15-a.service.requires/ + ln -sf ../test15-b.service /run/systemd/system/test15-c.service.requires/ + ln -sf /dev/null /etc/systemd/system/test15-c.service.requires/test15-b.service + systemctl start test15-a + check_ko test15-c Requires test15-b.service + systemctl stop test15-a test15-b + + # 'b' is already loaded when 'c' pulls it in via a dropin but 'b' is + # masked at a lower level. + echo "*** test a requires b is masked" + create_services test15-a test15-b test15-c + ln -sf ../test15-b.service /etc/systemd/system/test15-a.service.requires/ + ln -sf ../test15-b.service /etc/systemd/system/test15-c.service.requires/ + ln -sf /dev/null /run/systemd/system/test15-c.service.requires/test15-b.service + systemctl start test15-a + check_ok test15-c Requires test15-b.service + systemctl stop test15-a test15-b + + # 'a' requires 2 aliases of 'b' and one of them is a mask. + echo "*** test a requires alias of b, other alias masked" + create_services test15-a test15-b + ln -sf test15-b.service /etc/systemd/system/test15-b1.service + ln -sf test15-b.service /etc/systemd/system/test15-b2.service + ln -sf /dev/null /etc/systemd/system/test15-a.service.requires/test15-b1.service + ln -sf ../test15-b1.service /run/systemd/system/test15-a.service.requires/ + ln -sf ../test15-b2.service /usr/lib/systemd/system/test15-a.service.requires/ + check_ok test15-a Requires test15-b + + # Same as above but now 'b' is masked. + echo "*** test a requires alias of b, b dep masked" + create_services test15-a test15-b + ln -sf test15-b.service /etc/systemd/system/test15-b1.service + ln -sf test15-b.service /etc/systemd/system/test15-b2.service + ln -sf ../test15-b1.service /run/systemd/system/test15-a.service.requires/ + ln -sf ../test15-b2.service /usr/lib/systemd/system/test15-a.service.requires/ + ln -sf /dev/null /etc/systemd/system/test15-a.service.requires/test15-b.service + check_ok test15-a Requires test15-b + + clear_units test15-{a,b}.service +} + +testcase_invalid_dropins() { + echo "Testing invalid dropins..." + # Assertion failed on earlier versions, command exits unsuccessfully on later versions + systemctl cat nonexistent@.service || true + create_services a + systemctl daemon-reload + # Assertion failed on earlier versions, command exits unsuccessfully on later versions + systemctl cat a@.service || true + systemctl stop a + clear_units a.service + return 0 +} + +testcase_symlink_dropin_directory() { + # For issue #21920. + echo "Testing symlink drop-in directory..." + create_services test15-a + rmdir /{etc,run,usr/lib}/systemd/system/test15-a.service.d + mkdir -p /tmp/testsuite-15-test15-a-dropin-directory + ln -s /tmp/testsuite-15-test15-a-dropin-directory /etc/systemd/system/test15-a.service.d + cat >/tmp/testsuite-15-test15-a-dropin-directory/override.conf <<EOF +[Unit] +Description=hogehoge +EOF + ln -s /tmp/testsuite-15-test15-a-dropin-directory-nonexistent /run/systemd/system/test15-a.service.d + touch /tmp/testsuite-15-test15-a-dropin-directory-regular + ln -s /tmp/testsuite-15-test15-a-dropin-directory-regular /usr/lib/systemd/system/test15-a.service.d + check_ok test15-a Description hogehoge + + clear_units test15-a.service +} + +run_testcases + +touch /testok diff --git a/test/units/testsuite-16.service b/test/units/testsuite-16.service new file mode 100644 index 0000000..d5494ae --- /dev/null +++ b/test/units/testsuite-16.service @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-16-EXTEND-TIMEOUT +# Testsuite: Assess all other testsuite-*.services worked as expected + +Wants=success-all.service +Wants=success-start.service +Wants=success-runtime.service +Wants=success-stop.service +Wants=fail-start.service +Wants=fail-stop.service +Wants=fail-runtime.service +StopWhenUnneeded=yes + +[Service] +ExecStartPre=rm -f /failed /testok +Type=exec +TimeoutStartSec=infinity +ExecStartPre=/usr/lib/systemd/tests/testdata/units/%N.sh +ExecStart=true diff --git a/test/units/testsuite-16.sh b/test/units/testsuite-16.sh new file mode 100755 index 0000000..c60995a --- /dev/null +++ b/test/units/testsuite-16.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +rm -f /test.log + +TESTLOG=/test.log.XXXXXXXX + +wait_for() +{ + local service="${1:-wait_for: missing service argument}" + local result="${2:-success}" + local time="${3:-45}" + + while [[ ! -f /${service}.terminated && ! -f /${service}.success && $time -gt 0 ]]; do + sleep 1 + time=$((time - 1)) + done + + if [[ ! -f /${service}.${result} ]]; then + journalctl -u "${service/_/-}.service" >>"$TESTLOG" + fi +} + +wait_for_timeout() +{ + local unit="$1" + local time="$2" + + while [[ $time -gt 0 ]]; do + if [[ "$(systemctl show --property=Result "$unit")" == "Result=timeout" ]]; then + return 0 + fi + + sleep 1 + time=$((time - 1)) + done + + journalctl -u "$unit" >>"$TESTLOG" + + return 1 +} + +# This checks all stages, start, runtime and stop, can be extended by +# EXTEND_TIMEOUT_USEC + +wait_for success_all + +# These check that EXTEND_TIMEOUT_USEC that occurs at greater than the +# extend timeout interval but less then the stage limit (TimeoutStartSec, +# RuntimeMaxSec, TimeoutStopSec) still succeed. + +wait_for success_start +wait_for success_runtime +wait_for success_stop + +# These ensure that EXTEND_TIMEOUT_USEC will still timeout in the +# appropriate stage, after the stage limit, when the EXTEND_TIMEOUT_USEC +# message isn't sent within the extend timeout interval. + +wait_for fail_start startfail +wait_for fail_stop stopfail +wait_for fail_runtime runtimefail + +# These ensure that RuntimeMaxSec is honored for scope and service units +# when they are created. +runtime_max_sec=5 + +systemd-run \ + --property=RuntimeMaxSec=${runtime_max_sec}s \ + -u runtime-max-sec-test-1.service \ + /usr/bin/sh -c "while true; do sleep 1; done" +wait_for_timeout runtime-max-sec-test-1.service $((runtime_max_sec + 2)) + +systemd-run \ + --property=RuntimeMaxSec=${runtime_max_sec}s \ + --scope \ + -u runtime-max-sec-test-2.scope \ + /usr/bin/sh -c "while true; do sleep 1; done" & +wait_for_timeout runtime-max-sec-test-2.scope $((runtime_max_sec + 2)) + +# These ensure that RuntimeMaxSec is honored for scope and service +# units if the value is changed and then the manager is reloaded. +systemd-run \ + -u runtime-max-sec-test-3.service \ + /usr/bin/sh -c "while true; do sleep 1; done" +mkdir -p /etc/systemd/system/runtime-max-sec-test-3.service.d/ +cat > /etc/systemd/system/runtime-max-sec-test-3.service.d/override.conf << EOF +[Service] +RuntimeMaxSec=${runtime_max_sec}s +EOF +systemctl daemon-reload +wait_for_timeout runtime-max-sec-test-3.service $((runtime_max_sec + 2)) + +systemd-run \ + --scope \ + -u runtime-max-sec-test-4.scope \ + /usr/bin/sh -c "while true; do sleep 1; done" & + +# Wait until the unit is running to avoid race with creating the override. +until systemctl is-active runtime-max-sec-test-4.scope; do + sleep 1 +done +mkdir -p /etc/systemd/system/runtime-max-sec-test-4.scope.d/ +cat > /etc/systemd/system/runtime-max-sec-test-4.scope.d/override.conf << EOF +[Scope] +RuntimeMaxSec=${runtime_max_sec}s +EOF +systemctl daemon-reload +wait_for_timeout runtime-max-sec-test-4.scope $((runtime_max_sec + 2)) + +if [[ -f "$TESTLOG" ]]; then + # no mv + cp "$TESTLOG" /test.log + exit 1 +fi + +touch /testok diff --git a/test/units/testsuite-17.00.sh b/test/units/testsuite-17.00.sh new file mode 100755 index 0000000..d2aec60 --- /dev/null +++ b/test/units/testsuite-17.00.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Tests for issue #28588 and #28653. + +# On boot, services need to be started in the following order: +# 1. systemd-tmpfiles-setup-dev-early.service +# 2. systemd-sysusers.service +# 3. systemd-tmpfiles-setup-dev.service +# 4. systemd-udevd.service + +output="$(systemctl show --property After --value systemd-udevd.service)" +assert_in "systemd-tmpfiles-setup-dev-early.service" "$output" +assert_in "systemd-sysusers.service" "$output" +assert_in "systemd-tmpfiles-setup-dev.service" "$output" + +output="$(systemctl show --property After --value systemd-tmpfiles-setup-dev.service)" +assert_in "systemd-tmpfiles-setup-dev-early.service" "$output" +assert_in "systemd-sysusers.service" "$output" + +output="$(systemctl show --property After --value systemd-sysusers.service)" +assert_in "systemd-tmpfiles-setup-dev-early.service" "$output" + +check_owner_and_mode() { + local dev=${1?} + local user=${2?} + local group=${3?} + local mode=${4:-} + + if [[ -e "$dev" ]]; then + assert_in "$user" "$(stat --format=%U "$dev")" + assert_in "$group" "$(stat --format=%G "$dev")" + if [[ -n "$mode" ]]; then + assert_in "$mode" "$(stat --format=%#0a "$dev")" + fi + fi + + return 0 +} + +# Check owner and access mode specified in static-nodes-permissions.conf +check_owner_and_mode /dev/snd/seq root audio 0660 +check_owner_and_mode /dev/snd/timer root audio 0660 +check_owner_and_mode /dev/loop-control root disk 0660 +check_owner_and_mode /dev/net/tun root root 0666 +check_owner_and_mode /dev/fuse root root 0666 +check_owner_and_mode /dev/vfio/vfio root root 0666 +check_owner_and_mode /dev/kvm root kvm +check_owner_and_mode /dev/vhost-net root kvm +check_owner_and_mode /dev/vhost-vsock root kvm + +exit 0 diff --git a/test/units/testsuite-17.01.sh b/test/units/testsuite-17.01.sh new file mode 100755 index 0000000..44f36f5 --- /dev/null +++ b/test/units/testsuite-17.01.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +mkdir -p /run/udev/rules.d/ + +rm -f /run/udev/rules.d/50-testsuite.rules +udevadm control --reload +udevadm trigger --settle /dev/sda + +while : ; do + ( + udevadm info /dev/sda | grep -q -v SYSTEMD_WANTS=foobar.service + udevadm info /dev/sda | grep -q -v SYSTEMD_WANTS=waldo.service + systemctl show -p WantedBy foobar.service | grep -q -v sda + systemctl show -p WantedBy waldo.service | grep -q -v sda + ) && break + + sleep .5 +done + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM=="block", KERNEL=="sda", OPTIONS="log_level=debug" +ACTION!="remove", SUBSYSTEM=="block", KERNEL=="sda", ENV{SYSTEMD_WANTS}="foobar.service" +EOF +udevadm control --reload +udevadm trigger --settle /dev/sda + +while : ; do + ( + udevadm info /dev/sda | grep -q SYSTEMD_WANTS=foobar.service + udevadm info /dev/sda | grep -q -v SYSTEMD_WANTS=waldo.service + systemctl show -p WantedBy foobar.service | grep -q sda + systemctl show -p WantedBy waldo.service | grep -q -v sda + ) && break + + sleep .5 +done + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM=="block", KERNEL=="sda", OPTIONS="log_level=debug" +ACTION!="remove", SUBSYSTEM=="block", KERNEL=="sda", ENV{SYSTEMD_WANTS}="waldo.service" +EOF +udevadm control --reload +udevadm trigger --settle /dev/sda + +while : ; do + ( + udevadm info /dev/sda | grep -q -v SYSTEMD_WANTS=foobar.service + udevadm info /dev/sda | grep -q SYSTEMD_WANTS=waldo.service + systemctl show -p WantedBy foobar.service | grep -q -v sda + systemctl show -p WantedBy waldo.service | grep -q sda + ) && break + + sleep .5 +done + +rm /run/udev/rules.d/50-testsuite.rules + +udevadm control --reload +udevadm trigger --settle /dev/sda + +while : ; do + ( + udevadm info /dev/sda | grep -q -v SYSTEMD_WANTS=foobar.service + udevadm info /dev/sda | grep -q -v SYSTEMD_WANTS=waldo.service + systemctl show -p WantedBy foobar.service | grep -q -v sda + systemctl show -p WantedBy waldo.service | grep -q -v sda + ) && break + + sleep .5 +done + +exit 0 diff --git a/test/units/testsuite-17.02.sh b/test/units/testsuite-17.02.sh new file mode 100755 index 0000000..b232fca --- /dev/null +++ b/test/units/testsuite-17.02.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +# disable shellcheck warning about '"aaa"' type quotation +# shellcheck disable=SC2016 + +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +mkdir -p /run/udev/rules.d/ + +# test for ID_RENAMING= udev property and device unit state + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +ACTION=="remove", GOTO="hoge_end" +SUBSYSTEM!="net", GOTO="hoge_end" +KERNEL!="hoge", GOTO="hoge_end" + +OPTIONS="log_level=debug" + +# emulate renaming +ACTION=="online", ENV{ID_RENAMING}="1" + +LABEL="hoge_end" +EOF + +udevadm control --log-priority=debug --reload --timeout=30 + +ip link add hoge type dummy +udevadm wait --timeout=30 --settle /sys/devices/virtual/net/hoge +assert_not_in "ID_RENAMING=" "$(udevadm info /sys/devices/virtual/net/hoge)" +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "active" ]]; do sleep .5; done' + +udevadm trigger --action=online --settle /sys/devices/virtual/net/hoge +assert_in "ID_RENAMING=" "$(udevadm info /sys/devices/virtual/net/hoge)" +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "inactive" ]]; do sleep .5; done' + +udevadm trigger --action=move --settle /sys/devices/virtual/net/hoge +assert_not_in "ID_RENAMING=" "$(udevadm info /sys/devices/virtual/net/hoge)" +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "active" ]]; do sleep .5; done' + +# test for renaming interface with NAME= (issue #25106) + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +ACTION!="add", GOTO="hoge_end" +SUBSYSTEM!="net", GOTO="hoge_end" + +OPTIONS="log_level=debug" + +KERNEL=="hoge", NAME="foobar" +KERNEL=="foobar", NAME="hoge" + +LABEL="hoge_end" +EOF + +udevadm control --log-priority=debug --reload --timeout=30 + +udevadm trigger --action=add --settle /sys/devices/virtual/net/hoge +udevadm wait --timeout=30 --settle /sys/devices/virtual/net/foobar +assert_not_in "ID_RENAMING=" "$(udevadm info /sys/devices/virtual/net/foobar)" +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/foobar)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/foobar)" != "active" ]]; do sleep .5; done' + +udevadm trigger --action=add --settle /sys/devices/virtual/net/foobar +udevadm wait --timeout=30 --settle /sys/devices/virtual/net/hoge +assert_not_in "ID_RENAMING=" "$(udevadm info /sys/devices/virtual/net/hoge)" +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/foobar)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/foobar)" != "inactive" ]]; do sleep .5; done' + +# cleanup +rm -f /run/udev/rules.d/50-testsuite.rules +udevadm control --reload --timeout=30 + +# test for renaming interface with an external tool (issue #16967) + +ip link set hoge name foobar +udevadm wait --timeout=30 --settle /sys/devices/virtual/net/foobar +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/foobar)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/foobar)" != "active" ]]; do sleep .5; done' + +ip link set foobar name hoge +udevadm wait --timeout=30 --settle /sys/devices/virtual/net/hoge +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/hoge)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/hoge)" != "active" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/devices/virtual/net/foobar)" != "inactive" ]]; do sleep .5; done' +timeout 30 bash -c 'while [[ "$(systemctl show --property=ActiveState --value /sys/subsystem/net/devices/foobar)" != "inactive" ]]; do sleep .5; done' + +# cleanup +ip link del hoge + +# shellcheck disable=SC2317 +teardown_netif_renaming_conflict() { + set +ex + + if [[ -n "$KILL_PID" ]]; then + kill "$KILL_PID" + fi + + rm -rf "$TMPDIR" + + rm -f /run/udev/rules.d/50-testsuite.rules + udevadm control --reload --timeout=30 + + ip link del hoge + ip link del foobar +} + +test_netif_renaming_conflict() { + local since found= + + trap teardown_netif_renaming_conflict RETURN + + cat >/run/udev/rules.d/50-testsuite.rules <<EOF +ACTION!="add", GOTO="hoge_end" +SUBSYSTEM!="net", GOTO="hoge_end" + +OPTIONS="log_level=debug" + +KERNEL=="foobar", NAME="hoge" + +LABEL="hoge_end" +EOF + + udevadm control --log-priority=debug --reload --timeout=30 + + ip link add hoge type dummy + udevadm wait --timeout=30 --settle /sys/devices/virtual/net/hoge + + TMPDIR=$(mktemp -d -p /tmp udev-tests.XXXXXX) + udevadm monitor --udev --property --subsystem-match=net >"$TMPDIR"/monitor.txt & + KILL_PID="$!" + + # make sure that 'udevadm monitor' actually monitor uevents + sleep 1 + + since="$(date '+%H:%M:%S')" + + # add another interface which will conflict with an existing interface + ip link add foobar type dummy + + for _ in {1..40}; do + if ( + grep -q 'ACTION=add' "$TMPDIR"/monitor.txt + grep -q 'DEVPATH=/devices/virtual/net/foobar' "$TMPDIR"/monitor.txt + grep -q 'SUBSYSTEM=net' "$TMPDIR"/monitor.txt + grep -q 'INTERFACE=foobar' "$TMPDIR"/monitor.txt + grep -q 'ID_NET_DRIVER=dummy' "$TMPDIR"/monitor.txt + grep -q 'ID_NET_NAME=foobar' "$TMPDIR"/monitor.txt + # Even when network interface renaming is failed, SYSTEMD_ALIAS with the conflicting name will be broadcast. + grep -q 'SYSTEMD_ALIAS=/sys/subsystem/net/devices/hoge' "$TMPDIR"/monitor.txt + grep -q 'UDEV_WORKER_FAILED=1' "$TMPDIR"/monitor.txt + grep -q 'UDEV_WORKER_ERRNO=17' "$TMPDIR"/monitor.txt + grep -q 'UDEV_WORKER_ERRNO_NAME=EEXIST' "$TMPDIR"/monitor.txt + ); then + cat "$TMPDIR"/monitor.txt + found=1 + break + fi + sleep .5 + done + test -n "$found" + + timeout 30 bash -c "until journalctl _PID=1 _COMM=systemd --since $since | grep -q 'foobar: systemd-udevd failed to process the device, ignoring: File exists'; do sleep 1; done" + # check if the invalid SYSTEMD_ALIAS property for the interface foobar is ignored by PID1 + assert_eq "$(systemctl show --property=SysFSPath --value /sys/subsystem/net/devices/hoge)" "/sys/devices/virtual/net/hoge" +} + +test_netif_renaming_conflict + +exit 0 diff --git a/test/units/testsuite-17.03.sh b/test/units/testsuite-17.03.sh new file mode 100755 index 0000000..56e352e --- /dev/null +++ b/test/units/testsuite-17.03.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex + +TEST_RULE="/run/udev/rules.d/49-test.rules" +KILL_PID= + +setup() { + mkdir -p "${TEST_RULE%/*}" + [[ -e /etc/udev/udev.conf ]] && cp -f /etc/udev/udev.conf /etc/udev/udev.conf.bak + # Don't bother storing the coredumps in journal for this particular test + mkdir -p /run/systemd/coredump.conf.d/ + echo -ne "[Coredump]\nStorage=external\n" >/run/systemd/coredump.conf.d/99-storage-journal.conf + + cat >"${TEST_RULE}" <<EOF +ACTION=="add", SUBSYSTEM=="mem", KERNEL=="null", OPTIONS="log_level=debug" +ACTION=="add", SUBSYSTEM=="mem", KERNEL=="null", PROGRAM=="/bin/sleep 60" +EOF + cat >/etc/udev/udev.conf <<EOF +event_timeout=10 +timeout_signal=SIGABRT +EOF + + systemctl restart systemd-udevd.service +} + +# shellcheck disable=SC2317 +teardown() { + set +e + + if [[ -n "$KILL_PID" ]]; then + kill "$KILL_PID" + fi + + rm -rf "$TMPDIR" + rm -f "$TEST_RULE" + [[ -e /etc/udev/udev.conf.bak ]] && mv -f /etc/udev/udev.conf.bak /etc/udev/udev.conf + rm /run/systemd/coredump.conf.d/99-storage-journal.conf + systemctl restart systemd-udevd.service +} + +run_test() { + local since + + since="$(date '+%F %T')" + + TMPDIR=$(mktemp -d -p /tmp udev-tests.XXXXXX) + udevadm monitor --udev --property --subsystem-match=mem >"$TMPDIR"/monitor.txt & + KILL_PID="$!" + + SYSTEMD_LOG_LEVEL=debug udevadm trigger --verbose --action add /dev/null + + for _ in {1..40}; do + if coredumpctl --since "$since" --no-legend --no-pager | grep /bin/udevadm ; then + kill "$KILL_PID" + KILL_PID= + + cat "$TMPDIR"/monitor.txt + grep -q 'UDEV_WORKER_FAILED=1' "$TMPDIR"/monitor.txt + grep -q 'UDEV_WORKER_SIGNAL=6' "$TMPDIR"/monitor.txt + grep -q 'UDEV_WORKER_SIGNAL_NAME=ABRT' "$TMPDIR"/monitor.txt + return 0 + fi + sleep .5 + done + + return 1 +} + +trap teardown EXIT + +setup +run_test + +exit 0 diff --git a/test/units/testsuite-17.04.sh b/test/units/testsuite-17.04.sh new file mode 100755 index 0000000..d1c3c85 --- /dev/null +++ b/test/units/testsuite-17.04.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +mkdir -p /run/udev/rules.d/ + +test ! -f /run/udev/tags/added/c1:3 +test ! -f /run/udev/tags/changed/c1:3 +udevadm info /dev/null | grep -E 'E: (TAGS|CURRENT_TAGS)=.*:(added|changed):' && exit 1 + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM=="mem", KERNEL=="null", OPTIONS="log_level=debug" +ACTION=="add", SUBSYSTEM=="mem", KERNEL=="null", TAG+="added" +ACTION=="change", SUBSYSTEM=="mem", KERNEL=="null", TAG+="changed" +EOF + +udevadm control --reload +SYSTEMD_LOG_LEVEL=debug udevadm trigger --verbose --settle --action add /dev/null + +test -f /run/udev/tags/added/c1:3 +test ! -f /run/udev/tags/changed/c1:3 +udevadm info /dev/null | grep -q 'E: TAGS=.*:added:.*' +udevadm info /dev/null | grep -q 'E: CURRENT_TAGS=.*:added:.*' +udevadm info /dev/null | grep -q 'E: TAGS=.*:changed:.*' && { echo 'unexpected TAGS='; exit 1; } +udevadm info /dev/null | grep -q 'E: CURRENT_TAGS=.*:changed:.*' && { echo 'unexpected CURRENT_TAGS='; exit 1; } + +SYSTEMD_LOG_LEVEL=debug udevadm trigger --verbose --settle --action change /dev/null + +test -f /run/udev/tags/added/c1:3 +test -f /run/udev/tags/changed/c1:3 +udevadm info /dev/null | grep -q 'E: TAGS=.*:added:.*' +udevadm info /dev/null | grep -q 'E: CURRENT_TAGS=.*:added:.*' && { echo 'unexpected CURRENT_TAGS='; exit 1; } +udevadm info /dev/null | grep -q 'E: TAGS=.*:changed:.*' +udevadm info /dev/null | grep -q 'E: CURRENT_TAGS=.*:changed:.*' + +SYSTEMD_LOG_LEVEL=debug udevadm trigger --verbose --settle --action add /dev/null + +test -f /run/udev/tags/added/c1:3 +test -f /run/udev/tags/changed/c1:3 +udevadm info /dev/null | grep -q 'E: TAGS=.*:added:.*' +udevadm info /dev/null | grep -q 'E: CURRENT_TAGS=.*:added:.*' +udevadm info /dev/null | grep -q 'E: TAGS=.*:changed:.*' +udevadm info /dev/null | grep -q 'E: CURRENT_TAGS=.*:changed:.*' && { echo 'unexpected CURRENT_TAGS='; exit 1; } + +rm /run/udev/rules.d/50-testsuite.rules +udevadm control --reload + +exit 0 diff --git a/test/units/testsuite-17.05.sh b/test/units/testsuite-17.05.sh new file mode 100755 index 0000000..60be31a --- /dev/null +++ b/test/units/testsuite-17.05.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +mkdir -p /run/udev/rules.d/ + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM=="mem", KERNEL=="null", OPTIONS="log_level=debug" +ACTION=="add", SUBSYSTEM=="mem", KERNEL=="null", IMPORT{program}="/bin/echo -e HOGE=aa\\\\x20\\\\x20\\\\x20bb\nFOO=\\\\x20aaa\\\\x20\n\n\n" +EOF + +udevadm control --reload +SYSTEMD_LOG_LEVEL=debug udevadm trigger --verbose --settle --action add /dev/null + +test -f /run/udev/data/c1:3 +udevadm info /dev/null | grep -q 'E: HOGE=aa\\x20\\x20\\x20bb' +udevadm info /dev/null | grep -q 'E: FOO=\\x20aaa\\x20' + +rm /run/udev/rules.d/50-testsuite.rules +udevadm control --reload + +exit 0 diff --git a/test/units/testsuite-17.06.sh b/test/units/testsuite-17.06.sh new file mode 100755 index 0000000..6d83645 --- /dev/null +++ b/test/units/testsuite-17.06.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# tests for udev watch + +function check_validity() { + local f ID_OR_HANDLE + + for f in /run/udev/watch/*; do + ID_OR_HANDLE="$(readlink "$f")" + test -L "/run/udev/watch/${ID_OR_HANDLE}" + test "$(readlink "/run/udev/watch/${ID_OR_HANDLE}")" = "$(basename "$f")" + done +} + +function check() { + for _ in {1..2}; do + systemctl restart systemd-udevd.service + udevadm control --ping + udevadm settle + check_validity + + for _ in {1..2}; do + udevadm trigger -w --action add --subsystem-match=block + check_validity + done + + for _ in {1..2}; do + udevadm trigger -w --action change --subsystem-match=block + check_validity + done + done +} + +mkdir -p /run/udev/rules.d/ + +cat >/run/udev/rules.d/00-debug.rules <<EOF +SUBSYSTEM=="block", KERNEL=="sda*", OPTIONS="log_level=debug" +EOF + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +ACTION=="add", SUBSYSTEM=="block", KERNEL=="sda", OPTIONS:="watch" +EOF + +check + +MAJOR=$(udevadm info /dev/sda | grep -e '^E: MAJOR=' | sed -e 's/^E: MAJOR=//') +MINOR=$(udevadm info /dev/sda | grep -e '^E: MINOR=' | sed -e 's/^E: MINOR=//') +test -L "/run/udev/watch/b${MAJOR}:${MINOR}" + +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +ACTION=="change", SUBSYSTEM=="block", KERNEL=="sda", OPTIONS:="nowatch" +EOF + +check + +MAJOR=$(udevadm info /dev/sda | grep -e '^E: MAJOR=' | sed -e 's/^E: MAJOR=//') +MINOR=$(udevadm info /dev/sda | grep -e '^E: MINOR=' | sed -e 's/^E: MINOR=//') +test ! -e "/run/udev/watch/b${MAJOR}:${MINOR}" + +rm /run/udev/rules.d/00-debug.rules +rm /run/udev/rules.d/50-testsuite.rules + +udevadm control --reload +systemctl reset-failed systemd-udevd.service + +exit 0 diff --git a/test/units/testsuite-17.07.sh b/test/units/testsuite-17.07.sh new file mode 100755 index 0000000..629393a --- /dev/null +++ b/test/units/testsuite-17.07.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +wait_service_active() {( + set +ex + for i in {1..20}; do + (( i > 1 )) && sleep 0.5 + if systemctl --quiet is-active "${1?}"; then + return 0 + fi + done + return 1 +)} + +wait_service_inactive() {( + set +ex + for i in {1..20}; do + (( i > 1 )) && sleep 0.5 + systemctl --quiet is-active "${1?}" + if [[ "$?" == "3" ]]; then + return 0 + fi + done + return 1 +)} + +mkdir -p /run/systemd/system +cat >/run/systemd/system/both.service <<EOF +[Service] +ExecStart=sleep 1000 +EOF + +cat >/run/systemd/system/on-add.service <<EOF +[Service] +ExecStart=sleep 1000 +EOF + +cat >/run/systemd/system/on-change.service <<EOF +[Service] +ExecStart=sleep 1000 +EOF + +systemctl daemon-reload + +mkdir -p /run/udev/rules.d/ +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM=="net", KERNEL=="dummy9?", OPTIONS="log_level=debug" +SUBSYSTEM=="net", KERNEL=="dummy9?", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}+="both.service", ENV{SYSTEMD_WANTS}+="on-add.service" +SUBSYSTEM=="net", KERNEL=="dummy9?", ACTION=="change", TAG+="systemd", ENV{SYSTEMD_WANTS}+="both.service", ENV{SYSTEMD_WANTS}+="on-change.service" +EOF + +udevadm control --reload + +# StopWhenUnneeded=no +ip link add dummy99 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy99 +wait_service_active both.service +wait_service_active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service +systemctl stop both.service on-add.service + +udevadm trigger --action=change --settle /sys/class/net/dummy99 +udevadm info /sys/class/net/dummy99 +wait_service_active both.service +assert_rc 3 systemctl --quiet is-active on-add.service +wait_service_active on-change.service +systemctl stop both.service on-change.service + +ip link del dummy99 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy99 +assert_rc 3 systemctl --quiet is-active both.service +assert_rc 3 systemctl --quiet is-active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +# StopWhenUnneeded=yes +cat >/run/systemd/system/both.service <<EOF +[Unit] +StopWhenUnneeded=yes + +[Service] +ExecStart=sleep 1000 +Type=simple +EOF + +cat >/run/systemd/system/on-add.service <<EOF +[Unit] +StopWhenUnneeded=yes + +[Service] +ExecStart=sleep 1000 +Type=simple +EOF + +cat >/run/systemd/system/on-change.service <<EOF +[Unit] +StopWhenUnneeded=yes + +[Service] +ExecStart=echo changed +RemainAfterExit=true +Type=oneshot +EOF + +systemctl daemon-reload + +# StopWhenUnneeded=yes (single device, only add event) +ip link add dummy99 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy99 +wait_service_active both.service +wait_service_active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +ip link del dummy99 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy99 +wait_service_inactive both.service +wait_service_inactive on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +# StopWhenUnneeded=yes (single device, add and change event) +ip link add dummy99 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy99 +wait_service_active both.service +wait_service_active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +udevadm trigger --action=change --settle /sys/class/net/dummy99 +assert_rc 0 systemctl --quiet is-active both.service +wait_service_inactive on-add.service +wait_service_active on-change.service + +ip link del dummy99 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy99 +wait_service_inactive both.service +assert_rc 3 systemctl --quiet is-active on-add.service +wait_service_inactive on-change.service + +# StopWhenUnneeded=yes (multiple devices, only add events) +ip link add dummy99 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy99 +wait_service_active both.service +wait_service_active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +ip link add dummy98 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy98 +assert_rc 0 systemctl --quiet is-active both.service +assert_rc 0 systemctl --quiet is-active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +ip link del dummy99 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy99 +assert_rc 0 systemctl --quiet is-active both.service +assert_rc 0 systemctl --quiet is-active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +ip link del dummy98 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy98 +wait_service_inactive both.service +wait_service_inactive on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +# StopWhenUnneeded=yes (multiple devices, add and change events) +ip link add dummy99 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy99 +wait_service_active both.service +wait_service_active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +ip link add dummy98 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/dummy98 +assert_rc 0 systemctl --quiet is-active both.service +assert_rc 0 systemctl --quiet is-active on-add.service +assert_rc 3 systemctl --quiet is-active on-change.service + +udevadm trigger --action=change --settle /sys/class/net/dummy99 +assert_rc 0 systemctl --quiet is-active both.service +assert_rc 0 systemctl --quiet is-active on-add.service +wait_service_active on-change.service + +ip link del dummy98 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy98 +assert_rc 0 systemctl --quiet is-active both.service +wait_service_inactive on-add.service +assert_rc 0 systemctl --quiet is-active on-change.service + +ip link del dummy99 +udevadm wait --settle --timeout=30 --removed /sys/class/net/dummy99 +wait_service_inactive both.service +assert_rc 3 systemctl --quiet is-active on-add.service +wait_service_inactive on-change.service + +# cleanup +rm -f /run/udev/rules.d/50-testsuite.rules +udevadm control --reload + +rm -f /run/systemd/system/on-add.service +rm -f /run/systemd/system/on-change.service +systemctl daemon-reload + +exit 0 diff --git a/test/units/testsuite-17.08.sh b/test/units/testsuite-17.08.sh new file mode 100755 index 0000000..e570c69 --- /dev/null +++ b/test/units/testsuite-17.08.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# This is a test for issue #24518. + +mkdir -p /run/udev/rules.d/ +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM=="mem", KERNEL=="null", OPTIONS="log_level=debug", TAG+="systemd" +SUBSYSTEM=="mem", KERNEL=="null", ACTION=="add", SYMLINK+="test/symlink-to-null-on-add", ENV{SYSTEMD_ALIAS}+="/sys/test/alias-to-null-on-add" +SUBSYSTEM=="mem", KERNEL=="null", ACTION=="change", SYMLINK+="test/symlink-to-null-on-change", ENV{SYSTEMD_ALIAS}+="/sys/test/alias-to-null-on-change" +EOF + +udevadm control --reload + +udevadm trigger --settle --action add /dev/null +for i in {1..20}; do + ((i > 1)) && sleep .5 + + ( + systemctl -q is-active /dev/test/symlink-to-null-on-add + ! systemctl -q is-active /dev/test/symlink-to-null-on-change + systemctl -q is-active /sys/test/alias-to-null-on-add + ! systemctl -q is-active /sys/test/alias-to-null-on-change + ) && break +done +assert_rc 0 systemctl -q is-active /dev/test/symlink-to-null-on-add +assert_rc 3 systemctl -q is-active /dev/test/symlink-to-null-on-change +assert_rc 0 systemctl -q is-active /sys/test/alias-to-null-on-add +assert_rc 3 systemctl -q is-active /sys/test/alias-to-null-on-change + +udevadm trigger --settle --action change /dev/null +for i in {1..20}; do + ((i > 1)) && sleep .5 + + ( + ! systemctl -q is-active /dev/test/symlink-to-null-on-add + systemctl -q is-active /dev/test/symlink-to-null-on-change + ! systemctl -q is-active /sys/test/alias-to-null-on-add + systemctl -q is-active /sys/test/alias-to-null-on-change + ) && break +done +assert_rc 3 systemctl -q is-active /dev/test/symlink-to-null-on-add +assert_rc 0 systemctl -q is-active /dev/test/symlink-to-null-on-change +assert_rc 3 systemctl -q is-active /sys/test/alias-to-null-on-add +assert_rc 0 systemctl -q is-active /sys/test/alias-to-null-on-change + +udevadm trigger --settle --action add /dev/null +for i in {1..20}; do + ((i > 1)) && sleep .5 + + ( + systemctl -q is-active /dev/test/symlink-to-null-on-add + ! systemctl -q is-active /dev/test/symlink-to-null-on-change + systemctl -q is-active /sys/test/alias-to-null-on-add + ! systemctl -q is-active /sys/test/alias-to-null-on-change + ) && break +done +assert_rc 0 systemctl -q is-active /dev/test/symlink-to-null-on-add +assert_rc 3 systemctl -q is-active /dev/test/symlink-to-null-on-change +assert_rc 0 systemctl -q is-active /sys/test/alias-to-null-on-add +assert_rc 3 systemctl -q is-active /sys/test/alias-to-null-on-change + +# cleanup +rm -f /run/udev/rules.d/50-testsuite.rules +udevadm control --reload + +exit 0 diff --git a/test/units/testsuite-17.09.sh b/test/units/testsuite-17.09.sh new file mode 100755 index 0000000..9993196 --- /dev/null +++ b/test/units/testsuite-17.09.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# This is a test for issue #24987. + +mkdir -p /run/udev/rules.d/ +cat >/run/udev/rules.d/50-testsuite.rules <<EOF +SUBSYSTEM!="mem", GOTO="test-end" +KERNEL!="null", GOTO="test-end" +ACTION=="remove", GOTO="test-end" + +# add 100 * 100byte of properties +$(for i in {1..100}; do printf 'ENV{XXX%03i}="0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"\n' "$i"; done) + +LABEL="test-end" +EOF + +udevadm control --reload + +TMPDIR=$(mktemp -d -p /tmp udev-tests.XXXXXX) +SYSTEMD_LOG_LEVEL=debug udevadm monitor --udev --property --subsystem-match=mem >"$TMPDIR"/monitor.txt 2>&1 & +KILL_PID="$!" + +FOUND= +for _ in {1..40}; do + if grep -F 'UDEV - the event which udev sends out after rule processing' "$TMPDIR"/monitor.txt; then + FOUND=1 + break + fi + sleep .5 +done +[[ -n "$FOUND" ]] + +udevadm trigger --verbose --settle --action add /dev/null + +FOUND= +for _ in {1..40}; do + if ! grep -e 'UDEV *\[[0-9.]*\] *add *\/devices\/virtual\/mem\/null (mem)' "$TMPDIR"/monitor.txt; then + sleep .5 + continue + fi + + FOUND=1 + for i in {1..100}; do + if ! grep -F "$(printf 'XXX%03i=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' "$i")" "$TMPDIR"/monitor.txt; then + FOUND= + break + fi + done + if [[ -n "$FOUND" ]]; then + break; + fi + + sleep .5 +done +[[ -n "$FOUND" ]] + +# cleanup +rm -f /run/udev/rules.d/50-testsuite.rules +udevadm control --reload + +kill "$KILL_PID" +rm -rf "$TMPDIR" + +exit 0 diff --git a/test/units/testsuite-17.10.sh b/test/units/testsuite-17.10.sh new file mode 100755 index 0000000..f229dcf --- /dev/null +++ b/test/units/testsuite-17.10.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Coverage test for udevadm + +# shellcheck disable=SC2317 +cleanup_17_10() { + set +e + + losetup -d "$loopdev" + rm -f "$blk" + + ip link delete "$netdev" +} + +# Set up some test devices +trap cleanup_17_10 EXIT + +netdev=dummy17.10 +ip link add $netdev type dummy + +blk="$(mktemp)" +dd if=/dev/zero of="$blk" bs=1M count=1 +loopdev="$(losetup --show -f "$blk")" + +udevadm -h + +udevadm control -e +udevadm control -l emerg +udevadm control -l alert +udevadm control -l crit +udevadm control -l err +udevadm control -l warning +udevadm control -l notice +udevadm control --log-level info +udevadm control --log-level debug +(! udevadm control -l hello) +udevadm control -s +udevadm control -S +udevadm control -R +udevadm control -p HELLO=world +udevadm control -m 42 +udevadm control --ping +udevadm control -t 5 +udevadm control -h + +udevadm info /dev/null +udevadm info /sys/class/net/$netdev +udevadm info "$(systemd-escape -p --suffix device /sys/devices/virtual/net/$netdev)" +udevadm info --property DEVNAME /sys/class/net/$netdev +udevadm info --property DEVNAME --value /sys/class/net/$netdev +udevadm info --property HELLO /sys/class/net/$netdev +udevadm info -p class/net/$netdev +udevadm info -p /class/net/$netdev +udevadm info --json=off -p class/net/$netdev +udevadm info --json=pretty -p class/net/$netdev | jq . +udevadm info --json=short -p class/net/$netdev | jq . +udevadm info -n null +udevadm info -q all /sys/class/net/$netdev +udevadm info -q name /dev/null +udevadm info -q path /sys/class/net/$netdev +udevadm info -q property /sys/class/net/$netdev +udevadm info -q symlink /sys/class/net/$netdev +udevadm info -q name -r /dev/null +udevadm info --query symlink --root /sys/class/net/$netdev +(! udevadm info -q hello -r /sys/class/net/$netdev) +udevadm info -a /sys/class/net/$netdev +udevadm info -t >/dev/null +udevadm info --tree /sys/class/net/$netdev +udevadm info -x /sys/class/net/$netdev +udevadm info -x -q path /sys/class/net/$netdev +udevadm info -P TEST_ /sys/class/net/$netdev +udevadm info -d /dev/null +udevadm info -e >/dev/null +udevadm info -e --json=off >/dev/null +udevadm info -e --json=pretty | jq . >/dev/null +udevadm info -e --json=short | jq . >/dev/null +udevadm info -e --subsystem-match acpi >/dev/null +udevadm info -e --subsystem-nomatch acpi >/dev/null +udevadm info -e --attr-match ifindex=2 >/dev/null +udevadm info -e --attr-nomatch ifindex=2 >/dev/null +udevadm info -e --property-match SUBSYSTEM=acpi >/dev/null +udevadm info -e --tag-match systemd >/dev/null +udevadm info -e --sysname-match lo >/dev/null +udevadm info -e --name-match /sys/class/net/$netdev >/dev/null +udevadm info -e --parent-match /sys/class/net/$netdev >/dev/null +udevadm info -e --initialized-match >/dev/null +udevadm info -e --initialized-nomatch >/dev/null +# udevadm info -c +udevadm info -w /sys/class/net/$netdev +udevadm info --wait-for-initialization=5 /sys/class/net/$netdev +udevadm info -h + +assert_rc 124 timeout 1 udevadm monitor +assert_rc 124 timeout 1 udevadm monitor -k +assert_rc 124 timeout 1 udevadm monitor -u +assert_rc 124 timeout 1 udevadm monitor -s net +assert_rc 124 timeout 1 udevadm monitor --subsystem-match net/$netdev +assert_rc 124 timeout 1 udevadm monitor -t systemd +assert_rc 124 timeout 1 udevadm monitor --tag-match hello +udevadm monitor -h + +udevadm settle +udevadm settle -t 5 +udevadm settle -E /sys/class/net/$netdev +udevadm settle -h + +udevadm test /dev/null +udevadm info /sys/class/net/$netdev +udevadm test "$(systemd-escape -p --suffix device /sys/devices/virtual/net/$netdev)" +udevadm test -a add /sys/class/net/$netdev +udevadm test -a change /sys/class/net/$netdev +udevadm test -a move /sys/class/net/$netdev +udevadm test -a online /sys/class/net/$netdev +udevadm test -a offline /sys/class/net/$netdev +udevadm test -a bind /sys/class/net/$netdev +udevadm test -a unbind /sys/class/net/$netdev +udevadm test -a help /sys/class/net/$netdev +udevadm test --action help +(! udevadm test -a hello /sys/class/net/$netdev) +udevadm test -N early /sys/class/net/$netdev +udevadm test -N late /sys/class/net/$netdev +udevadm test --resolve-names never /sys/class/net/$netdev +(! udevadm test -N hello /sys/class/net/$netdev) +udevadm test -h + +# udevadm test-builtin path_id "$loopdev" +udevadm test-builtin net_id /sys/class/net/$netdev +udevadm test-builtin net_id "$(systemd-escape -p --suffix device /sys/devices/virtual/net/$netdev)" +udevadm test-builtin -a add net_id /sys/class/net/$netdev +udevadm test-builtin -a remove net_id /sys/class/net/$netdev +udevadm test-builtin -a change net_id /sys/class/net/$netdev +udevadm test-builtin -a move net_id /sys/class/net/$netdev +udevadm test-builtin -a online net_id /sys/class/net/$netdev +udevadm test-builtin -a offline net_id /sys/class/net/$netdev +udevadm test-builtin -a bind net_id /sys/class/net/$netdev +udevadm test-builtin -a unbind net_id /sys/class/net/$netdev +udevadm test-builtin -a help net_id /sys/class/net/$netdev +udevadm test-builtin net_setup_link /sys/class/net/$netdev +udevadm test-builtin blkid "$loopdev" +udevadm test-builtin input_id /sys/class/net/$netdev +udevadm test-builtin keyboard /dev/null +# udevadm test-builtin kmod /sys/class/net/$netdev +udevadm test-builtin uaccess /dev/null +# udevadm test-builtin usb_id dev/null +(! udevadm test-builtin hello /sys/class/net/$netdev) +# systemd-hwdb update is extremely slow when combined with sanitizers and run +# in a VM without acceleration, so let's just skip the one particular test +# if we detect this combination +if ! [[ -v ASAN_OPTIONS && "$(systemd-detect-virt -v)" == "qemu" ]]; then + modprobe scsi_debug + scsidev=$(readlink -f /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/[0-9]*) + mkdir -p /etc/udev/hwdb.d + cat >/etc/udev/hwdb.d/99-test.hwdb <<EOF +scsi:* + ID_TEST=test +EOF + systemd-hwdb update + + udevadm test-builtin hwdb "$scsidev" + + rmmod scsi_debug || : + rm -fv /etc/udev/hwdb.d/99-test.hwdb + systemd-hwdb update +fi + + +udevadm trigger +udevadm trigger /dev/null +udevadm trigger /sys/class/net/$netdev +udevadm trigger "$(systemd-escape -p --suffix device /sys/devices/virtual/net/$netdev)" +udevadm trigger -v /sys/class/net/$netdev +udevadm trigger -n /sys/class/net/$netdev +udevadm trigger -q /sys/class/net/$netdev +udevadm trigger -t all /sys/class/net/$netdev +udevadm trigger -t devices /sys/class/net/$netdev +udevadm trigger --type subsystems /sys/class/net/$netdev +(! udevadm trigger -t hello /sys/class/net/$netdev) +udevadm trigger -c add /sys/class/net/$netdev +udevadm trigger -c remove /sys/class/net/$netdev +udevadm trigger -c change /sys/class/net/$netdev +udevadm trigger -c move /sys/class/net/$netdev +udevadm trigger -c online /sys/class/net/$netdev +udevadm trigger -c offline /sys/class/net/$netdev +udevadm trigger -c bind /sys/class/net/$netdev +udevadm trigger -c unbind /sys/class/net/$netdev +udevadm trigger -c help /sys/class/net/$netdev +udevadm trigger --action help /sys/class/net/$netdev +(! udevadm trigger -c hello /sys/class/net/$netdev) +udevadm trigger --prioritized-subsystem block +udevadm trigger --prioritized-subsystem block,net +udevadm trigger --prioritized-subsystem hello +udevadm trigger -s net +udevadm trigger -S net +udevadm trigger -a subsystem=net +udevadm trigger --attr-match hello=world +udevadm trigger -p DEVNAME=null +udevadm trigger --property-match HELLO=world +udevadm trigger -g systemd +udevadm trigger --tag-match hello +udevadm trigger -y net +udevadm trigger --sysname-match hello +udevadm trigger --name-match /sys/class/net/$netdev +udevadm trigger --name-match /sys/class/net/$netdev --name-match /dev/null +udevadm trigger -b /sys/class/net/$netdev +udevadm trigger --parent-match /sys/class/net/$netdev --name-match /dev/null +udevadm trigger --initialized-match +udevadm trigger --initialized-nomatch +udevadm trigger -w +udevadm trigger --uuid /sys/class/net/$netdev +udevadm settle -t 300 +udevadm trigger --wait-daemon +udevadm settle -t 300 +udevadm trigger --wait-daemon=5 +udevadm trigger -h + +# https://github.com/systemd/systemd/issues/29863 +if [[ "$(systemd-detect-virt -v)" != "qemu" ]]; then + udevadm control --log-level=0 + for _ in {0..9}; do + timeout 30 udevadm trigger --settle + done + udevadm control --log-level=debug +fi + +udevadm wait /dev/null +udevadm wait /sys/class/net/$netdev +udevadm wait -t 5 /sys/class/net/$netdev +udevadm wait --initialized true /sys/class/net/$netdev +udevadm wait --initialized false /sys/class/net/$netdev +(! udevadm wait --initialized hello /sys/class/net/$netdev) +assert_rc 124 timeout 5 udevadm wait --removed /sys/class/net/$netdev +udevadm wait --settle /sys/class/net/$netdev +udevadm wait -h + +udevadm lock --help +udevadm lock --version +for i in /dev/block/*; do + udevadm lock --device "$i" --print + udevadm lock --device "$i" true + (! udevadm lock --device "$i" false) +done +for i in / /usr; do + udevadm lock --backing "$i" --print + udevadm lock --backing "$i" true + (! udevadm lock --backing "$i" false) +done + +exit 0 diff --git a/test/units/testsuite-17.11.sh b/test/units/testsuite-17.11.sh new file mode 100755 index 0000000..42b925f --- /dev/null +++ b/test/units/testsuite-17.11.sh @@ -0,0 +1,447 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# Test for udevadm verify. + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# shellcheck disable=SC2317 +cleanup() { + cd / + rm -rf "${workdir}" + workdir= +} + +workdir="$(mktemp -d)" +trap cleanup EXIT +cd "${workdir}" + +cat >"${workdir}/default_output_1_success" <<EOF + +1 udev rules files have been checked. + Success: 1 + Fail: 0 +EOF +cat >"${workdir}/default_output_1_fail" <<EOF + +1 udev rules files have been checked. + Success: 0 + Fail: 1 +EOF +cat >"${workdir}/output_0_files" <<EOF + +0 udev rules files have been checked. + Success: 0 + Fail: 0 +EOF + +test_number=0 +rules= +exp= +err= +out= +next_test_number() { + : $((++test_number)) + + local num_str + num_str=$(printf %05d "${test_number}") + + rules="sample-${num_str}.rules" + exp="sample-${num_str}.exp" + err="sample-${num_str}.err" + exo="sample-${num_str}.exo" + out="sample-${num_str}.out" +} + +assert_0_impl() { + udevadm verify "$@" >"${out}" + if [ -f "${exo}" ]; then + diff -u "${exo}" "${out}" + elif [ -f "${rules}" ]; then + diff -u "${workdir}/default_output_1_success" "${out}" + fi +} + +assert_0() { + assert_0_impl "$@" + next_test_number +} + +assert_1_impl() { + local rc + set +e + udevadm verify "$@" >"${out}" 2>"${err}" + rc=$? + set -e + assert_eq "$rc" 1 + + if [ -f "${exp}" ]; then + diff -u "${exp}" "${err}" + fi + + if [ -f "${exo}" ]; then + diff -u "${exo}" "${out}" + elif [ -f "${rules}" ]; then + diff -u "${workdir}/default_output_1_fail" "${out}" + fi +} + +assert_1() { + assert_1_impl "$@" + next_test_number +} + +# initialize variables +next_test_number + +assert_0 -h +assert_0 --help +assert_0 -V +assert_0 --version +assert_0 /dev/null + +# unrecognized option '--unknown' +assert_1 --unknown +# option requires an argument -- 'N' +assert_1 -N +# --resolve-names= takes "early" or "never" +assert_1 -N now +# option '--resolve-names' requires an argument +assert_1 --resolve-names +# --resolve-names= takes "early" or "never" +assert_1 --resolve-names=now +# Failed to parse rules file ./nosuchfile: No such file or directory +assert_1 ./nosuchfile +# Failed to parse rules file ./nosuchfile: No such file or directory +cat >"${exo}" <<EOF + +3 udev rules files have been checked. + Success: 2 + Fail: 1 +EOF +assert_1 /dev/null ./nosuchfile /dev/null + +rules_dir='etc/udev/rules.d' +mkdir -p "${rules_dir}" +# No rules files found in $PWD +assert_1 --root="${workdir}" + +# Directory without rules. +cp "${workdir}/output_0_files" "${exo}" +assert_0 "${rules_dir}" + +# Directory with a loop. +ln -s . "${rules_dir}/loop.rules" +assert_1 "${rules_dir}" +rm "${rules_dir}/loop.rules" + +# Empty rules. +touch "${rules_dir}/empty.rules" +assert_0 --root="${workdir}" +: >"${exo}" +assert_0 --root="${workdir}" --no-summary + +# Directory with a single *.rules file. +cp "${workdir}/default_output_1_success" "${exo}" +assert_0 "${rules_dir}" + +# Combination of --root= and FILEs is not supported. +assert_1 --root="${workdir}" /dev/null +# No rules files found in nosuchdir +assert_1 --root=nosuchdir + +cd "${rules_dir}" + +# UDEV_LINE_SIZE 16384 +printf '%16383s\n' ' ' >"${rules}" +assert_0 "${rules}" + +# Failed to parse rules file ${rules}: No buffer space available +printf '%16384s\n' ' ' >"${rules}" +echo "Failed to parse rules file ${rules}: No buffer space available" >"${exp}" +assert_1 "${rules}" + +{ + printf 'RUN+="/bin/true",%8174s\\\n' ' ' + printf 'RUN+="/bin/false"%8174s\\\n' ' ' + echo +} >"${rules}" +assert_0 "${rules}" + +printf 'RUN+="/bin/true"%8176s\\\n #\n' ' ' ' ' >"${rules}" +echo >>"${rules}" +cat >"${exp}" <<EOF +${rules}:5 Line is too long, ignored. +${rules}: udev rules check failed. +EOF +assert_1 "${rules}" + +printf '\\\n' >"${rules}" +cat >"${exp}" <<EOF +${rules}:1 Unexpected EOF after line continuation, line ignored. +${rules}: udev rules check failed. +EOF +assert_1 "${rules}" + +test_syntax_error() { + local rule msg + + rule="$1"; shift + msg="$1"; shift + + printf '%s\n' "${rule}" >"${rules}" + cat >"${exp}" <<EOF +${rules}:1 ${msg} +${rules}: udev rules check failed. +EOF + assert_1 "${rules}" +} + +test_style_error() { + local rule msg + + rule="$1"; shift + msg="$1"; shift + + printf '%s\n' "${rule}" >"${rules}" + cat >"${exp}" <<EOF +${rules}:1 ${msg} +${rules}: udev rules have style issues. +EOF + assert_0_impl --no-style "${rules}" + assert_1_impl "${rules}" + next_test_number +} + +test_syntax_error '=' 'Invalid key/value pair, ignoring.' +test_syntax_error 'ACTION{a}=="b"' 'Invalid attribute for ACTION.' +test_syntax_error 'ACTION:="b"' 'Invalid operator for ACTION.' +test_syntax_error 'ACTION=="b"' 'The line has no effect, ignoring.' +test_syntax_error 'DEVPATH{a}=="b"' 'Invalid attribute for DEVPATH.' +test_syntax_error 'DEVPATH:="b"' 'Invalid operator for DEVPATH.' +test_syntax_error 'KERNEL{a}=="b"' 'Invalid attribute for KERNEL.' +test_syntax_error 'KERNEL:="b"' 'Invalid operator for KERNEL.' +test_syntax_error 'KERNELS{a}=="b"' 'Invalid attribute for KERNELS.' +test_syntax_error 'KERNELS:="b"' 'Invalid operator for KERNELS.' +test_syntax_error 'SYMLINK{a}=="b"' 'Invalid attribute for SYMLINK.' +test_syntax_error 'SYMLINK:="%?"' 'Invalid value "%?" for SYMLINK (char 1: invalid substitution type), ignoring.' +test_syntax_error 'NAME{a}=="b"' 'Invalid attribute for NAME.' +test_syntax_error 'NAME-="b"' 'Invalid operator for NAME.' +test_syntax_error 'NAME+="a"' "NAME key takes '==', '!=', '=', or ':=' operator, assuming '='." +test_syntax_error 'NAME:=""' 'Ignoring NAME="", as udev will not delete any network interfaces.' +test_syntax_error 'NAME="%k"' 'Ignoring NAME="%k", as it will take no effect.' +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 '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.' +test_syntax_error 'TAG{a}=="b"' 'Invalid attribute for TAG.' +test_syntax_error 'TAG:="a"' "TAG key takes '==', '!=', '=', or '+=' operator, assuming '='." +test_syntax_error 'TAG="%?"' 'Invalid value "%?" for TAG (char 1: invalid substitution type), ignoring.' +test_syntax_error 'TAGS{a}=="b"' 'Invalid attribute for TAGS.' +test_syntax_error 'TAGS:="a"' 'Invalid operator for TAGS.' +test_syntax_error 'SUBSYSTEM{a}=="b"' 'Invalid attribute for SUBSYSTEM.' +test_syntax_error 'SUBSYSTEM:="b"' 'Invalid operator for SUBSYSTEM.' +test_syntax_error 'SUBSYSTEM=="bus", NAME="b"' '"bus" must be specified as "subsystem".' +test_syntax_error 'SUBSYSTEMS{a}=="b"' 'Invalid attribute for SUBSYSTEMS.' +test_syntax_error 'SUBSYSTEMS:="b"' 'Invalid operator for SUBSYSTEMS.' +test_syntax_error 'DRIVER{a}=="b"' 'Invalid attribute for DRIVER.' +test_syntax_error 'DRIVER:="b"' 'Invalid operator for DRIVER.' +test_syntax_error 'DRIVERS{a}=="b"' 'Invalid attribute for DRIVERS.' +test_syntax_error 'DRIVERS:="b"' 'Invalid operator for DRIVERS.' +test_syntax_error 'ATTR="b"' 'Invalid attribute for ATTR.' +test_syntax_error 'ATTR{%}="b"' 'Invalid attribute "%" for ATTR (char 1: invalid substitution type), ignoring.' +test_syntax_error 'ATTR{a}-="b"' 'Invalid operator for ATTR.' +test_syntax_error 'ATTR{a}+="b"' "ATTR key takes '==', '!=', or '=' operator, assuming '='." +test_syntax_error 'ATTR{a}="%?"' 'Invalid value "%?" for ATTR (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SYSCTL=""' 'Invalid attribute for SYSCTL.' +test_syntax_error 'SYSCTL{%}="b"' 'Invalid attribute "%" for SYSCTL (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SYSCTL{a}-="b"' 'Invalid operator for SYSCTL.' +test_syntax_error 'SYSCTL{a}+="b"' "SYSCTL key takes '==', '!=', or '=' operator, assuming '='." +test_syntax_error 'SYSCTL{a}="%?"' 'Invalid value "%?" for SYSCTL (char 1: invalid substitution type), ignoring.' +test_syntax_error 'ATTRS=""' 'Invalid attribute for ATTRS.' +test_syntax_error 'ATTRS{%}=="b", NAME="b"' 'Invalid attribute "%" for ATTRS (char 1: invalid substitution type), ignoring.' +test_syntax_error 'ATTRS{a}-="b"' 'Invalid operator for ATTRS.' +test_syntax_error 'ATTRS{device/}!="a", NAME="b"' "'device' link may not be available in future kernels." +test_syntax_error 'ATTRS{../}!="a", NAME="b"' 'Direct reference to parent sysfs directory, may break in future kernels.' +test_syntax_error 'TEST{a}=="b"' "Failed to parse mode 'a': Invalid argument" +test_syntax_error 'TEST{0}=="%", NAME="b"' 'Invalid value "%" for TEST (char 1: invalid substitution type), ignoring.' +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 '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{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.' +test_syntax_error 'OPTIONS{a}="b"' 'Invalid attribute for OPTIONS.' +test_syntax_error 'OPTIONS-="b"' 'Invalid operator for OPTIONS.' +test_syntax_error 'OPTIONS!="b"' 'Invalid operator for OPTIONS.' +test_syntax_error 'OPTIONS+="link_priority=a"' "Failed to parse link priority 'a': Invalid argument" +test_syntax_error 'OPTIONS:="log_level=a"' "Failed to parse log level 'a': Invalid argument" +test_syntax_error 'OPTIONS="a", NAME="b"' "Invalid value for OPTIONS key, ignoring: 'a'" +test_syntax_error 'OWNER{a}="b"' 'Invalid attribute for OWNER.' +test_syntax_error 'OWNER-="b"' 'Invalid operator for OWNER.' +test_syntax_error 'OWNER!="b"' 'Invalid operator for OWNER.' +test_syntax_error 'OWNER+="0"' "OWNER key takes '=' or ':=' operator, assuming '='." +test_syntax_error 'OWNER=":nosuchuser:"' "Unknown user ':nosuchuser:', ignoring." +test_syntax_error 'GROUP{a}="b"' 'Invalid attribute for GROUP.' +test_syntax_error 'GROUP-="b"' 'Invalid operator for GROUP.' +test_syntax_error 'GROUP!="b"' 'Invalid operator for GROUP.' +test_syntax_error 'GROUP+="0"' "GROUP key takes '=' or ':=' operator, assuming '='." +test_syntax_error 'GROUP=":nosuchgroup:"' "Unknown group ':nosuchgroup:', ignoring." +test_syntax_error 'MODE{a}="b"' 'Invalid attribute for MODE.' +test_syntax_error 'MODE-="b"' 'Invalid operator for MODE.' +test_syntax_error 'MODE!="b"' 'Invalid operator for MODE.' +test_syntax_error 'MODE+="0"' "MODE key takes '=' or ':=' operator, assuming '='." +test_syntax_error 'MODE="%"' 'Invalid value "%" for MODE (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SECLABEL="b"' 'Invalid attribute for SECLABEL.' +test_syntax_error 'SECLABEL{a}="%"' 'Invalid value "%" for SECLABEL (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SECLABEL{a}!="b"' 'Invalid operator for SECLABEL.' +test_syntax_error 'SECLABEL{a}-="b"' 'Invalid operator for SECLABEL.' +test_syntax_error 'SECLABEL{a}:="b"' "SECLABEL key takes '=' or '+=' operator, assuming '='." +test_syntax_error 'RUN=="b"' 'Invalid operator for RUN.' +test_syntax_error 'RUN-="b"' 'Invalid operator for RUN.' +test_syntax_error 'RUN="%"' 'Invalid value "%" for RUN (char 1: invalid substitution type), ignoring.' +test_syntax_error 'RUN{builtin}+="foo"' "Unknown builtin command 'foo', ignoring." +test_syntax_error 'GOTO{a}="b"' 'Invalid attribute for GOTO.' +test_syntax_error 'GOTO=="b"' 'Invalid operator for GOTO.' +test_syntax_error 'NAME="a", GOTO="b"' 'GOTO="b" has no matching label, ignoring.' +test_syntax_error 'GOTO="a", GOTO="b" +LABEL="a"' 'Contains multiple GOTO keys, ignoring GOTO="b".' +test_syntax_error 'LABEL{a}="b"' 'Invalid attribute for LABEL.' +test_syntax_error 'LABEL=="b"' 'Invalid operator for LABEL.' +test_style_error 'LABEL="b"' 'style: LABEL="b" is unused.' +test_syntax_error 'a="b"' "Invalid key 'a'." +test_syntax_error 'KERNEL=="", KERNEL=="?*", NAME="a"' 'conflicting match expressions, the line has no effect.' +test_syntax_error 'KERNEL=="abc", KERNEL!="abc", NAME="b"' 'conflicting match expressions, the line has no effect.' +test_syntax_error 'KERNEL=="|a|b", KERNEL!="b|a|", NAME="c"' 'conflicting match expressions, the line has no effect.' +test_syntax_error 'KERNEL=="a|b", KERNEL=="c|d|e", NAME="f"' 'conflicting match expressions, the line has no effect.' +# shellcheck disable=SC2016 +test_syntax_error 'ENV{DISKSEQ}=="?*", ENV{DEVTYPE}!="partition", ENV{DISKSEQ}!="?*", ENV{ID_IGNORE_DISKSEQ}!="1", SYMLINK+="disk/by-diskseq/$env{DISKSEQ}"' \ + 'conflicting match expressions, the line has no effect.' +test_syntax_error 'ACTION=="a*", ACTION=="bc*", NAME="d"' 'conflicting match expressions, the line has no effect.' +test_syntax_error 'ACTION=="a*|bc*", ACTION=="d*|ef*", NAME="g"' 'conflicting match expressions, the line has no effect.' +test_syntax_error 'KERNEL!="", KERNEL=="?*", NAME="a"' 'duplicate expressions.' +test_syntax_error 'KERNEL=="|a|b", KERNEL=="b|a|", NAME="c"' 'duplicate expressions.' +# shellcheck disable=SC2016 +test_syntax_error 'ENV{DISKSEQ}=="?*", ENV{DEVTYPE}!="partition", ENV{DISKSEQ}=="?*", ENV{ID_IGNORE_DISKSEQ}!="1", SYMLINK+="disk/by-diskseq/$env{DISKSEQ}"' \ + 'duplicate expressions.' +test_style_error ',ACTION=="a", NAME="b"' 'style: stray leading comma.' +test_style_error ' ,ACTION=="a", NAME="b"' 'style: stray leading comma.' +test_style_error ', ACTION=="a", NAME="b"' 'style: stray leading comma.' +test_style_error 'ACTION=="a", NAME="b",' 'style: stray trailing comma.' +test_style_error 'ACTION=="a", NAME="b", ' 'style: stray trailing comma.' +test_style_error 'ACTION=="a" NAME="b"' 'style: a comma between tokens is expected.' +test_style_error 'ACTION=="a",, NAME="b"' 'style: more than one comma between tokens.' +test_style_error 'ACTION=="a" , NAME="b"' 'style: stray whitespace before comma.' +test_style_error 'ACTION=="a",NAME="b"' 'style: whitespace after comma is expected.' +test_syntax_error 'RESULT=="a", PROGRAM="b"' 'Reordering RESULT check after PROGRAM assignment.' +test_syntax_error 'RESULT=="a*", PROGRAM="b", RESULT=="*c", PROGRAM="d"' \ + 'Reordering RESULT check after PROGRAM assignment.' + +cat >"${rules}" <<'EOF' +KERNEL=="a|b", KERNEL=="a|c", NAME="d" +KERNEL=="a|b", KERNEL!="a|c", NAME="d" +KERNEL!="a", KERNEL!="b", NAME="c" +KERNEL=="|a", KERNEL=="|b", NAME="c" +KERNEL=="*", KERNEL=="a*", NAME="b" +KERNEL=="a*", KERNEL=="c*|ab*", NAME="d" +PROGRAM="a", RESULT=="b" +EOF +assert_0 "${rules}" + +echo 'GOTO="a"' >"${rules}" +cat >"${exp}" <<EOF +${rules}:1 GOTO="a" has no matching label, ignoring. +${rules}:1 The line has no effect any more, dropping. +${rules}: udev rules check failed. +EOF +assert_1 "${rules}" + +cat >"${rules}" <<'EOF' +GOTO="a" +LABEL="a" +EOF +assert_0 "${rules}" + +cat >"${rules}" <<'EOF' +GOTO="b" +LABEL="b" +LABEL="b" +EOF +cat >"${exp}" <<EOF +${rules}:3 style: LABEL="b" is unused. +${rules}: udev rules have style issues. +EOF +assert_0_impl --no-style "${rules}" +assert_1_impl "${rules}" + +cat >"${rules}" <<'EOF' +GOTO="a" +LABEL="a", LABEL="b" +EOF +cat >"${exp}" <<EOF +${rules}:2 Contains multiple LABEL keys, ignoring LABEL="a". +${rules}:1 GOTO="a" has no matching label, ignoring. +${rules}:1 The line has no effect any more, dropping. +${rules}:2 style: LABEL="b" is unused. +${rules}: udev rules check failed. +EOF +assert_1 "${rules}" + +cat >"${rules}" <<'EOF' +KERNEL!="", KERNEL=="?*", KERNEL=="", NAME="a" +EOF +cat >"${exp}" <<EOF +${rules}:1 duplicate expressions. +${rules}:1 conflicting match expressions, the line has no effect. +${rules}: udev rules check failed. +EOF +assert_1 "${rules}" + +cat >"${rules}" <<'EOF' +ACTION=="a"NAME="b" +EOF +cat >"${exp}" <<EOF +${rules}:1 style: a comma between tokens is expected. +${rules}:1 style: whitespace between tokens is expected. +${rules}: udev rules have style issues. +EOF +assert_0_impl --no-style "${rules}" +assert_1_impl "${rules}" +next_test_number + +cat >"${rules}" <<'EOF' +ACTION=="a" ,NAME="b" +EOF +cat >"${exp}" <<EOF +${rules}:1 style: stray whitespace before comma. +${rules}:1 style: whitespace after comma is expected. +${rules}: udev rules have style issues. +EOF +assert_0_impl --no-style "${rules}" +assert_1_impl "${rules}" +next_test_number + +# udevadm verify --root +sed "s|sample-[0-9]*.rules|${workdir}/${rules_dir}/&|" sample-*.exp >"${workdir}/${exp}" +cd - +assert_1 --root="${workdir}" +cd - + +# udevadm verify path/ +sed "s|sample-[0-9]*.rules|${workdir}/${rules_dir}/&|" sample-*.exp >"${workdir}/${exp}" +cd - +assert_1 "${rules_dir}" +cd - + +exit 0 diff --git a/test/units/testsuite-17.12.sh b/test/units/testsuite-17.12.sh new file mode 100755 index 0000000..ccc91bf --- /dev/null +++ b/test/units/testsuite-17.12.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +create_link_file() { + name=${1?} + + mkdir -p /run/systemd/network/ + cat >/run/systemd/network/10-test.link <<EOF +[Match] +Kind=dummy +MACAddress=00:50:56:c0:00:18 + +[Link] +Name=$name +AlternativeName=test1 test2 test3 test4 +EOF + udevadm control --reload +} + +udevadm control --log-level=debug + +create_link_file test1 +ip link add address 00:50:56:c0:00:18 type dummy +udevadm wait --settle --timeout=30 /sys/class/net/test1 +output=$(ip link show dev test1) +if ! [[ "$output" =~ altname ]]; then + echo "alternative name for network interface not supported, skipping test." + exit 0 +fi +assert_not_in "altname test1" "$output" +assert_in "altname test2" "$output" +assert_in "altname test3" "$output" +assert_in "altname test4" "$output" + +# By triggering add event, Name= and AlternativeNames= are re-applied +create_link_file test2 +udevadm trigger --action add --settle /sys/class/net/test1 +udevadm wait --settle --timeout=30 /sys/class/net/test2 +output=$(ip link show dev test2) +assert_in "altname test1" "$output" +assert_not_in "altname test2" "$output" +assert_in "altname test3" "$output" +assert_in "altname test4" "$output" + +# Name= and AlternativeNames= are not applied on move event +create_link_file test3 +udevadm trigger --action move --settle /sys/class/net/test2 +udevadm wait --settle --timeout=30 /sys/class/net/test2 +output=$(ip link show dev test2) +assert_in "altname test1" "$output" +assert_not_in "altname test2" "$output" +assert_in "altname test3" "$output" +assert_in "altname test4" "$output" + +# Test move event triggered by manual renaming +ip link set dev test2 name hoge +udevadm wait --settle --timeout=30 /sys/class/net/hoge +output=$(ip link show dev hoge) +assert_in "altname test1" "$output" +assert_not_in "altname test2" "$output" +assert_in "altname test3" "$output" +assert_in "altname test4" "$output" +assert_not_in "altname hoge" "$output" + +# Re-test add event +udevadm trigger --action add --settle /sys/class/net/hoge +udevadm wait --settle --timeout=30 /sys/class/net/test3 +output=$(ip link show dev test3) +assert_in "altname test1" "$output" +assert_in "altname test2" "$output" +assert_not_in "altname test3" "$output" +assert_in "altname test4" "$output" +assert_not_in "altname hoge" "$output" + +# cleanup +ip link del dev test3 + +rm -f /run/systemd/network/10-test.link +udevadm control --reload --log-level=info + +exit 0 diff --git a/test/units/testsuite-17.13.sh b/test/units/testsuite-17.13.sh new file mode 100755 index 0000000..d9dfdd7 --- /dev/null +++ b/test/units/testsuite-17.13.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Test for `udevadm control -p` + +test_not_property() { + assert_eq "$(udevadm info --query property --property "$2" --value "$1")" "" +} + +test_property() { + assert_eq "$(udevadm info --query property --property "$2" --value "$1")" "$3" +} + +# shellcheck disable=SC2317 +cleanup() { + set +e + + udevadm control -p FOO= -p BAR= + + rm -f "$rules" +} + +# Set up a test device +trap cleanup EXIT + +rules="/run/udev/rules.d/99-test-17.13.rules" + +mkdir -p "${rules%/*}" +cat > "$rules" <<'EOF' +ENV{FOO}=="?*", ENV{PROP_FOO}="$env{FOO}" +ENV{BAR}=="?*", ENV{PROP_BAR}="$env{BAR}" +EOF + +udevadm control --reload + +test_not_property /dev/null PROP_FOO +test_not_property /dev/null PROP_BAR + +: Setting of a property works + +udevadm control --property FOO=foo +udevadm trigger --action change --settle /dev/null +test_property /dev/null PROP_FOO foo +test_not_property /dev/null PROP_BAR + +: Change of a property works + +udevadm control --property FOO=goo +udevadm trigger --action change --settle /dev/null +test_property /dev/null PROP_FOO goo + +: Removal of a property works + +udevadm control --property FOO= +udevadm trigger --action change --settle /dev/null +test_not_property /dev/null PROP_FOO + +: Repeated removal of a property does nothing + +udevadm control --property FOO= +udevadm trigger --action change --settle /dev/null +test_not_property /dev/null PROP_FOO + +: Multiple properties can be set at once + +udevadm control --property FOO=foo --property BAR=bar +udevadm trigger --action change --settle /dev/null +test_property /dev/null PROP_FOO foo +test_property /dev/null PROP_BAR bar + +: Multiple setting of the same property is handled correctly + +udevadm control --property FOO=foo --property FOO=42 +udevadm trigger --action change --settle /dev/null +test_property /dev/null PROP_FOO 42 + +: Mix of settings and removals of the same property is handled correctly + +udevadm control -p FOO= -p FOO=foo -p BAR=car -p BAR= +udevadm trigger --action change --settle /dev/null +test_property /dev/null PROP_FOO foo +test_not_property /dev/null PROP_BAR + +exit 0 diff --git a/test/units/testsuite-17.service b/test/units/testsuite-17.service new file mode 100644 index 0000000..d218d72 --- /dev/null +++ b/test/units/testsuite-17.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-17-UDEV + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-17.sh b/test/units/testsuite-17.sh new file mode 100755 index 0000000..14ceeba --- /dev/null +++ b/test/units/testsuite-17.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +udevadm settle + +run_subtests + +touch /testok diff --git a/test/units/testsuite-18.service b/test/units/testsuite-18.service new file mode 100644 index 0000000..16d90a1 --- /dev/null +++ b/test/units/testsuite-18.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-18-FAILUREACTION + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-18.sh b/test/units/testsuite-18.sh new file mode 100755 index 0000000..44b792f --- /dev/null +++ b/test/units/testsuite-18.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-run --wait -p FailureAction=poweroff true +(! systemd-run --wait -p SuccessAction=poweroff false) + +if ! test -f /firstphase ; then + echo OK >/firstphase + systemd-run --wait -p SuccessAction=reboot true +else + echo OK >/testok + systemd-run --wait -p FailureAction=poweroff false +fi + +sleep infinity diff --git a/test/units/testsuite-19.ExitType-cgroup.sh b/test/units/testsuite-19.ExitType-cgroup.sh new file mode 100755 index 0000000..cd221d7 --- /dev/null +++ b/test/units/testsuite-19.ExitType-cgroup.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -eux + +# Test ExitType=cgroup + +# 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 + +systemd-analyze log-level debug + +# Multiple level process tree, parent process stays up +cat >/tmp/test19-exit-cgroup.sh <<EOF +#!/usr/bin/env bash +set -eux + +# process tree: systemd -> sleep +sleep infinity & +disown + +# process tree: systemd -> bash -> bash -> sleep +((sleep infinity); true) & + +systemd-notify --ready + +# Run the stop/kill command +\$1 & + +# process tree: systemd -> bash -> sleep +sleep infinity +EOF +chmod +x /tmp/test19-exit-cgroup.sh + +# service should be stopped cleanly +systemd-run --wait \ + --unit=one \ + --property="Type=notify" \ + --property="ExitType=cgroup" \ + /tmp/test19-exit-cgroup.sh 'systemctl stop one' + +# same thing with a truthy exec condition +systemd-run --wait \ + --unit=two \ + --property="Type=notify" \ + --property="ExitType=cgroup" \ + --property="ExecCondition=true" \ + /tmp/test19-exit-cgroup.sh 'systemctl stop two' + +# false exec condition: systemd-run should exit immediately with status code: 1 +(! systemd-run --wait \ + --unit=three \ + --property="Type=notify" \ + --property="ExitType=cgroup" \ + --property="ExecCondition=false" \ + /tmp/test19-exit-cgroup.sh) + +# service should exit uncleanly (main process exits with SIGKILL) +(! systemd-run --wait \ + --unit=four \ + --property="Type=notify" \ + --property="ExitType=cgroup" \ + /tmp/test19-exit-cgroup.sh 'systemctl kill --signal 9 four') + + +# Multiple level process tree, parent process exits quickly +cat >/tmp/test19-exit-cgroup-parentless.sh <<EOF +#!/usr/bin/env bash +set -eux + +# process tree: systemd -> sleep +sleep infinity & + +# process tree: systemd -> bash -> sleep +((sleep infinity); true) & + +systemd-notify --ready + +# Run the stop/kill command after this bash process exits +(sleep 1; \$1) & +EOF +chmod +x /tmp/test19-exit-cgroup-parentless.sh + +# service should be stopped cleanly +systemd-run --wait \ + --unit=five \ + --property="Type=notify" \ + --property="ExitType=cgroup" \ + /tmp/test19-exit-cgroup-parentless.sh 'systemctl stop five' + +# service should still exit cleanly despite SIGKILL (the main process already exited cleanly) +systemd-run --wait \ + --unit=six \ + --property="Type=notify" \ + --property="ExitType=cgroup" \ + /tmp/test19-exit-cgroup-parentless.sh 'systemctl kill --signal 9 six' + + +systemd-analyze log-level info diff --git a/test/units/testsuite-19.cleanup-slice.sh b/test/units/testsuite-19.cleanup-slice.sh new file mode 100755 index 0000000..5d63160 --- /dev/null +++ b/test/units/testsuite-19.cleanup-slice.sh @@ -0,0 +1,49 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug + +# Create service with KillMode=none inside a slice +cat <<EOF >/run/systemd/system/test19cleanup.service +[Unit] +Description=Test 19 cleanup Service +[Service] +Slice=test19cleanup.slice +Type=exec +ExecStart=sleep infinity +KillMode=none +EOF +cat <<EOF >/run/systemd/system/test19cleanup.slice +[Unit] +Description=Test 19 cleanup Slice +EOF + +# Start service +systemctl start test19cleanup.service +assert_rc 0 systemd-cgls /test19cleanup.slice + +pid=$(systemctl show --property MainPID --value test19cleanup) +ps "$pid" + +# Stop slice +# The sleep process will not be killed because of KillMode=none +# Since there is still a process running under it, the /test19cleanup.slice cgroup won't be removed +systemctl stop test19cleanup.slice + +ps "$pid" + +# Kill sleep process manually +kill -s TERM "$pid" +while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done + +timeout 30 bash -c 'while systemd-cgls /test19cleanup.slice/test19cleanup.service >& /dev/null; do sleep .5; done' +assert_rc 1 systemd-cgls /test19cleanup.slice/test19cleanup.service + +# Check that empty cgroup /test19cleanup.slice has been removed +timeout 30 bash -c 'while systemd-cgls /test19cleanup.slice >& /dev/null; do sleep .5; done' +assert_rc 1 systemd-cgls /test19cleanup.slice diff --git a/test/units/testsuite-19.delegate.sh b/test/units/testsuite-19.delegate.sh new file mode 100755 index 0000000..74d36c4 --- /dev/null +++ b/test/units/testsuite-19.delegate.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test cgroup delegation in the unified hierarchy + +# 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 + +at_exit() { + set +e + userdel -r test +} + +systemd-run --wait \ + --unit=test-0.service \ + --property="DynamicUser=1" \ + --property="Delegate=" \ + test -w /sys/fs/cgroup/system.slice/test-0.service/ -a \ + -w /sys/fs/cgroup/system.slice/test-0.service/cgroup.procs -a \ + -w /sys/fs/cgroup/system.slice/test-0.service/cgroup.subtree_control + +# Test if this also works for some of the more recent attrs the kernel might or might not support +for attr in cgroup.threads memory.oom.group memory.reclaim ; do + + if grep -q "$attr" /sys/kernel/cgroup/delegate ; then + systemd-run --wait \ + --unit=test-0.service \ + --property="DynamicUser=1" \ + --property="Delegate=" \ + test -w /sys/fs/cgroup/system.slice/test-0.service/ -a \ + -w /sys/fs/cgroup/system.slice/test-0.service/"$attr" + fi +done + +systemd-run --wait \ + --unit=test-1.service \ + --property="DynamicUser=1" \ + --property="Delegate=memory pids" \ + grep -q memory /sys/fs/cgroup/system.slice/test-1.service/cgroup.controllers + +systemd-run --wait \ + --unit=test-2.service \ + --property="DynamicUser=1" \ + --property="Delegate=memory pids" \ + grep -q pids /sys/fs/cgroup/system.slice/test-2.service/cgroup.controllers + +# "io" is not among the controllers enabled by default for all units, verify that +grep -qv io /sys/fs/cgroup/system.slice/cgroup.controllers + +# Run a service with "io" enabled, and verify it works +systemd-run --wait \ + --unit=test-3.service \ + --property="IOAccounting=yes" \ + --property="Slice=system-foo-bar-baz.slice" \ + grep -q io /sys/fs/cgroup/system.slice/system-foo.slice/system-foo-bar.slice/system-foo-bar-baz.slice/test-3.service/cgroup.controllers + +# We want to check if "io" is removed again from the controllers +# list. However, PID 1 (rightfully) does this asynchronously. In order +# to force synchronization on this, let's start a short-lived service +# which requires PID 1 to refresh the cgroup tree, so that we can +# verify that this all works. +systemd-run --wait --unit=test-4.service true + +# And now check again, "io" should have vanished +grep -qv io /sys/fs/cgroup/system.slice/cgroup.controllers + +# Check that unprivileged delegation works for scopes +useradd test ||: +systemd-run --uid=test \ + --property="User=test" \ + --property="Delegate=yes" \ + --slice workload.slice \ + --unit test-workload0.scope\ + --scope \ + test -w /sys/fs/cgroup/workload.slice/test-workload0.scope -a \ + -w /sys/fs/cgroup/workload.slice/test-workload0.scope/cgroup.procs -a \ + -w /sys/fs/cgroup/workload.slice/test-workload0.scope/cgroup.subtree_control + +# Verify that DelegateSubgroup= affects ownership correctly +unit="test-subgroup-$RANDOM.service" +systemd-run --wait \ + --unit="$unit" \ + --property="DynamicUser=1" \ + --property="Delegate=pids" \ + --property="DelegateSubgroup=foo" \ + test -w "/sys/fs/cgroup/system.slice/$unit" -a \ + -w "/sys/fs/cgroup/system.slice/$unit/foo" + +# Check that for the subgroup also attributes that aren't covered by +# regular (i.e. main cgroup) delegation ownership rules are delegated properly +if test -f /sys/fs/cgroup/cgroup.max.depth; then + unit="test-subgroup-$RANDOM.service" + systemd-run --wait \ + --unit="$unit" \ + --property="DynamicUser=1" \ + --property="Delegate=pids" \ + --property="DelegateSubgroup=zzz" \ + test -w "/sys/fs/cgroup/system.slice/$unit/zzz/cgroup.max.depth" +fi + +# Check that the invoked process itself is also in the subgroup +unit="test-subgroup-$RANDOM.service" +systemd-run --wait \ + --unit="$unit" \ + --property="DynamicUser=1" \ + --property="Delegate=pids" \ + --property="DelegateSubgroup=bar" \ + grep -q -x -F "0::/system.slice/$unit/bar" /proc/self/cgroup diff --git a/test/units/testsuite-19.service b/test/units/testsuite-19.service new file mode 100644 index 0000000..9ee5fc9 --- /dev/null +++ b/test/units/testsuite-19.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-19-DELEGATE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-19.sh b/test/units/testsuite-19.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-19.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-21.service b/test/units/testsuite-21.service new file mode 100644 index 0000000..a5f77d0 --- /dev/null +++ b/test/units/testsuite-21.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Fuzz our D-Bus interfaces with dfuzzer +After=dbus.service multi-user.target +Wants=dbus.service multi-user.target + +[Service] +ExecStartPre=rm -f /failed /skipped /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-21.sh b/test/units/testsuite-21.sh new file mode 100755 index 0000000..02673ab --- /dev/null +++ b/test/units/testsuite-21.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Save the end.service state before we start fuzzing, as it might get changed +# on the fly by one of the fuzzers +systemctl list-jobs | grep -F 'end.service' && SHUTDOWN_AT_EXIT=1 || SHUTDOWN_AT_EXIT=0 + +# shellcheck disable=SC2317 +at_exit() { + set +e + # We have to call the end.service/poweroff explicitly even if it's specified on + # the kernel cmdline via systemd.wants=end.service, since dfuzzer calls + # org.freedesktop.systemd1.Manager.ClearJobs() which drops the service + # from the queue + if [[ $SHUTDOWN_AT_EXIT -ne 0 ]] && ! systemctl poweroff; then + # PID1 is down let's try to save the journal + journalctl --sync # journal can be down as well so let's ignore exit codes here + systemctl -ff poweroff # sync() and reboot(RB_POWER_OFF) + fi +} + +trap at_exit EXIT + +systemctl log-level info + +# FIXME: systemd-run doesn't play well with daemon-reexec +# See: https://github.com/systemd/systemd/issues/27204 +sed -i '/\[org.freedesktop.systemd1\]/aorg.freedesktop.systemd1.Manager:Reexecute FIXME' /etc/dfuzzer.conf +sed -i '/\[org.freedesktop.systemd1\]/aorg.freedesktop.systemd1.Manager:SoftReboot destructive' /etc/dfuzzer.conf + +# TODO +# * check for possibly newly introduced buses? +BUS_LIST=( + org.freedesktop.home1 + org.freedesktop.hostname1 + org.freedesktop.import1 + org.freedesktop.locale1 + org.freedesktop.login1 + org.freedesktop.machine1 + org.freedesktop.portable1 + org.freedesktop.resolve1 + org.freedesktop.systemd1 + org.freedesktop.timedate1 +) + +# systemd-oomd requires PSI +if tail -n +1 /proc/pressure/{cpu,io,memory}; then + BUS_LIST+=( + org.freedesktop.oom1 + ) +fi + +# Some services require specific conditions: +# - systemd-timesyncd can't run in a container +# - systemd-networkd can run in a container if it has CAP_NET_ADMIN capability +if ! systemd-detect-virt --container; then + BUS_LIST+=( + org.freedesktop.network1 + org.freedesktop.timesync1 + ) +elif busctl introspect org.freedesktop.network1 / &>/dev/null; then + BUS_LIST+=( + org.freedesktop.network1 + ) +fi + +SESSION_BUS_LIST=( + org.freedesktop.systemd1 +) + +# Maximum payload size generated by dfuzzer (in bytes) - default: 50K +PAYLOAD_MAX=50000 +# Tweak the maximum payload size if we're running under sanitizers, since +# with larger payloads we start hitting reply timeouts +if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then + PAYLOAD_MAX=10000 # 10K +fi + +# Overmount /var/lib/machines with a size-limited tmpfs, as fuzzing +# the org.freedesktop.machine1 stuff makes quite a mess +mount -t tmpfs -o size=50M tmpfs /var/lib/machines + +# Fuzz both the system and the session buses (where applicable) +for bus in "${BUS_LIST[@]}"; do + echo "Bus: $bus (system)" + systemd-run --pipe --wait \ + -- dfuzzer -b "$PAYLOAD_MAX" -n "$bus" + + # Let's reload the systemd daemon to test (de)serialization as well + systemctl daemon-reload + # FIXME: explicitly trigger reexecute until systemd/systemd#27204 is resolved + systemctl daemon-reexec +done + +umount /var/lib/machines + +for bus in "${SESSION_BUS_LIST[@]}"; do + echo "Bus: $bus (session)" + systemd-run --machine 'testuser@.host' --user --pipe --wait \ + -- dfuzzer -b "$PAYLOAD_MAX" -n "$bus" + + # Let's reload the systemd user daemon to test (de)serialization as well + systemctl --machine 'testuser@.host' --user daemon-reload + # FIXME: explicitly trigger reexecute until systemd/systemd#27204 is resolved + systemctl --machine 'testuser@.host' --user daemon-reexec +done + +touch /testok diff --git a/test/units/testsuite-22.01.sh b/test/units/testsuite-22.01.sh new file mode 100755 index 0000000..2276b75 --- /dev/null +++ b/test/units/testsuite-22.01.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# With "e" don't attempt to set permissions when file doesn't exist, see +# https://github.com/systemd/systemd/pull/6682. +set -eux +set -o pipefail + +rm -fr /tmp/test + +echo "e /tmp/test - root root 1d" | systemd-tmpfiles --create - + +test ! -e /tmp/test diff --git a/test/units/testsuite-22.02.sh b/test/units/testsuite-22.02.sh new file mode 100755 index 0000000..b883a96 --- /dev/null +++ b/test/units/testsuite-22.02.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Basic tests for types creating directories +set -eux +set -o pipefail + +rm -fr /tmp/{C,d,D,e} +mkdir /tmp/{C,d,D,e} + +# +# 'd' +# +mkdir /tmp/d/2 +chmod 777 /tmp/d/2 + +systemd-tmpfiles --create - <<EOF +d /tmp/d/1 0755 daemon daemon - - +d /tmp/d/2 0755 daemon daemon - - +EOF + +test -d /tmp/d/1 +test "$(stat -c %U:%G:%a /tmp/d/1)" = "daemon:daemon:755" + +test -d /tmp/d/2 +test "$(stat -c %U:%G:%a /tmp/d/2)" = "daemon:daemon:755" + +# +# 'D' +# +mkdir /tmp/D/2 +chmod 777 /tmp/D/2 +touch /tmp/D/2/foo + +systemd-tmpfiles --create - <<EOF +D /tmp/D/1 0755 daemon daemon - - +D /tmp/D/2 0755 daemon daemon - - +EOF + +test -d /tmp/D/1 +test "$(stat -c %U:%G:%a /tmp/D/1)" = "daemon:daemon:755" + +test -d /tmp/D/2 +test "$(stat -c %U:%G:%a /tmp/D/2)" = "daemon:daemon:755" + +systemd-tmpfiles --remove - <<EOF +D /tmp/D/2 0755 daemon daemon - - +EOF + +# the content of '2' should be removed +test "$(echo /tmp/D/2/*)" = "/tmp/D/2/*" + +# +# 'e' +# +mkdir -p /tmp/e/2/{d1,d2} +chmod 777 /tmp/e/2 +chmod 777 /tmp/e/2/d* + +systemd-tmpfiles --create - <<EOF +e /tmp/e/1 0755 daemon daemon - - +e /tmp/e/2/* 0755 daemon daemon - - +EOF + +test ! -d /tmp/e/1 + +test -d /tmp/e/2 +test "$(stat -c %U:%G:%a /tmp/e/2)" = "root:root:777" + +test -d /tmp/e/2/d1 +test "$(stat -c %U:%G:%a /tmp/e/2/d1)" = "daemon:daemon:755" +test -d /tmp/e/2/d2 +test "$(stat -c %U:%G:%a /tmp/e/2/d2)" = "daemon:daemon:755" + +# 'e' operates on directories only +mkdir -p /tmp/e/3/{d1,d2} +chmod 777 /tmp/e/3 +chmod 777 /tmp/e/3/d* +touch /tmp/e/3/f1 +chmod 644 /tmp/e/3/f1 + +systemd-tmpfiles --create - <<EOF +e /tmp/e/3/* 0755 daemon daemon - - +EOF + +# the directories should have been processed although systemd-tmpfiles failed +# previously due to the presence of a file. +test -d /tmp/e/3/d1 +test "$(stat -c %U:%G:%a /tmp/e/3/d1)" = "daemon:daemon:755" +test -d /tmp/e/3/d2 +test "$(stat -c %U:%G:%a /tmp/e/3/d2)" = "daemon:daemon:755" + +test -f /tmp/e/3/f1 +test "$(stat -c %U:%G:%a /tmp/e/3/f1)" = "root:root:644" + +# +# 'C' +# + +mkdir /tmp/C/{0,1,2,3}-origin +touch /tmp/C/{1,2,3}-origin/f1 +chmod 755 /tmp/C/{1,2,3}-origin/f1 + +mkdir /tmp/C/{2,3} +touch /tmp/C/3/f1 + +systemd-tmpfiles --create - <<EOF +C /tmp/C/1 0755 daemon daemon - /tmp/C/1-origin +C /tmp/C/2 0755 daemon daemon - /tmp/C/2-origin +EOF + +test -d /tmp/C/1 +test "$(stat -c %U:%G:%a /tmp/C/1/f1)" = "daemon:daemon:755" +test -d /tmp/C/2 +test "$(stat -c %U:%G:%a /tmp/C/2/f1)" = "daemon:daemon:755" + +systemd-tmpfiles --create - <<EOF +C /tmp/C/3 0755 daemon daemon - /tmp/C/3-origin +C /tmp/C/4 0755 daemon daemon - /tmp/C/definitely-missing +EOF + +test "$(stat -c %U:%G:%a /tmp/C/3/f1)" = "root:root:644" +test ! -e /tmp/C/4 + +touch /tmp/C/3-origin/f{2,3,4} +echo -n ABC > /tmp/C/3/f1 + +systemd-tmpfiles --create - <<EOF +C+ /tmp/C/3 0755 daemon daemon - /tmp/C/3-origin +EOF + +# Test that the trees got merged, even though /tmp/C/3 already exists. +test -e /tmp/C/3/f1 +test -e /tmp/C/3/f2 +test -e /tmp/C/3/f3 +test -e /tmp/C/3/f4 + +# Test that /tmp/C/3/f1 did not get overwritten. +test "$(cat /tmp/C/3/f1)" = "ABC" + +# Check that %U expands to 0, both in the path and in the argument. +home='/tmp/C' +systemd-tmpfiles --create - <<EOF +C $home/%U - - - - $home/%U-origin +EOF + +test -d "$home/0" + +# Check that %h expands to $home, both in the path and in the argument. +HOME="$home" \ +systemd-tmpfiles --create - <<EOF +C %h/5 - - - - %h/3-origin +EOF + +test -f "$home/5/f1" + +# Check that %h in the path is expanded, but +# the result of this expansion is not expanded once again. +root='/tmp/C/6' +home='/%U' +mkdir -p "$root/usr/share/factory$home" +HOME="$home" \ +systemd-tmpfiles --create --root="$root" - <<EOF +C %h - - - - +EOF + +test -d "$root$home" diff --git a/test/units/testsuite-22.03.sh b/test/units/testsuite-22.03.sh new file mode 100755 index 0000000..6fce4c0 --- /dev/null +++ b/test/units/testsuite-22.03.sh @@ -0,0 +1,246 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Basic tests for types creating/writing files +set -eux +set -o pipefail + +rm -fr /tmp/{f,F,w} +mkdir /tmp/{f,F,w} +touch /tmp/file-owned-by-root + +# +# 'f' +# +systemd-tmpfiles --create - <<EOF +f /tmp/f/1 0644 - - - - +f /tmp/f/2 0644 - - - This string should be written +EOF + +### '1' should exist and be empty +test -f /tmp/f/1; test ! -s /tmp/f/1 +test "$(stat -c %U:%G:%a /tmp/f/1)" = "root:root:644" + +test "$(stat -c %U:%G:%a /tmp/f/2)" = "root:root:644" +test "$(< /tmp/f/2)" = "This string should be written" + +### The perms are supposed to be updated even if the file already exists. +systemd-tmpfiles --create - <<EOF +f /tmp/f/1 0666 daemon daemon - This string should not be written +EOF + +# file should be empty +test ! -s /tmp/f/1 +test "$(stat -c %U:%G:%a /tmp/f/1)" = "daemon:daemon:666" + +### But we shouldn't try to set perms on an existing file which is not a +### regular one. +mkfifo /tmp/f/fifo +chmod 644 /tmp/f/fifo + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/f/fifo 0666 daemon daemon - This string should not be written +EOF + +test -p /tmp/f/fifo +test "$(stat -c %U:%G:%a /tmp/f/fifo)" = "root:root:644" + +### 'f' should not follow symlinks. +ln -s missing /tmp/f/dangling +ln -s /tmp/file-owned-by-root /tmp/f/symlink + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/f/dangling 0644 daemon daemon - - +f /tmp/f/symlink 0644 daemon daemon - - +EOF +test ! -e /tmp/f/missing +test "$(stat -c %U:%G:%a /tmp/file-owned-by-root)" = "root:root:644" + +### Handle read-only filesystem gracefully: we shouldn't fail if the target +### already exists and have the correct perms. +mkdir /tmp/f/rw-fs +mkdir /tmp/f/ro-fs + +touch /tmp/f/rw-fs/foo +chmod 644 /tmp/f/rw-fs/foo + +mount -o bind,ro /tmp/f/rw-fs /tmp/f/ro-fs + +systemd-tmpfiles --create - <<EOF +f /tmp/f/ro-fs/foo 0644 - - - - This string should not be written +EOF +test -f /tmp/f/ro-fs/foo; test ! -s /tmp/f/ro-fs/foo + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/f/ro-fs/foo 0666 - - - - +EOF +test "$(stat -c %U:%G:%a /tmp/f/fifo)" = "root:root:644" + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/f/ro-fs/bar 0644 - - - - +EOF +test ! -e /tmp/f/ro-fs/bar + +### 'f' shouldn't follow unsafe paths. +mkdir /tmp/f/daemon +ln -s /root /tmp/f/daemon/unsafe-symlink +chown -R --no-dereference daemon:daemon /tmp/f/daemon + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/f/daemon/unsafe-symlink/exploit 0644 daemon daemon - - +EOF +test ! -e /tmp/f/daemon/unsafe-symlink/exploit + +# +# 'F' +# +echo "This should be truncated" >/tmp/F/truncated +echo "This should be truncated" >/tmp/F/truncated-with-content + +systemd-tmpfiles --create - <<EOF +F /tmp/F/created 0644 - - - - +F /tmp/F/created-with-content 0644 - - - new content +F /tmp/F/truncated 0666 daemon daemon - - +F /tmp/F/truncated-with-content 0666 daemon daemon - new content +EOF + +test -f /tmp/F/created; test ! -s /tmp/F/created +test -f /tmp/F/created-with-content +test "$(< /tmp/F/created-with-content)" = "new content" +test -f /tmp/F/truncated; test ! -s /tmp/F/truncated +test "$(stat -c %U:%G:%a /tmp/F/truncated)" = "daemon:daemon:666" +test -s /tmp/F/truncated-with-content +test "$(stat -c %U:%G:%a /tmp/F/truncated-with-content)" = "daemon:daemon:666" + +### We shouldn't try to truncate anything but regular files since the behavior is +### unspecified in the other cases. +mkfifo /tmp/F/fifo + +(! systemd-tmpfiles --create -) <<EOF +F /tmp/F/fifo 0644 - - - - +EOF + +test -p /tmp/F/fifo + +### 'F' should not follow symlinks. +ln -s missing /tmp/F/dangling +ln -s /tmp/file-owned-by-root /tmp/F/symlink + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/F/dangling 0644 daemon daemon - - +f /tmp/F/symlink 0644 daemon daemon - - +EOF +test ! -e /tmp/F/missing +test "$(stat -c %U:%G:%a /tmp/file-owned-by-root)" = "root:root:644" + +### Handle read-only filesystem gracefully: we shouldn't fail if the target +### already exists and is empty. +mkdir /tmp/F/rw-fs +mkdir /tmp/F/ro-fs + +touch /tmp/F/rw-fs/foo +chmod 644 /tmp/F/rw-fs/foo + +mount -o bind,ro /tmp/F/rw-fs /tmp/F/ro-fs + +systemd-tmpfiles --create - <<EOF +F /tmp/F/ro-fs/foo 0644 - - - - +EOF +test -f /tmp/F/ro-fs/foo; test ! -s /tmp/F/ro-fs/foo + +echo "truncating is not allowed anymore" >/tmp/F/rw-fs/foo +(! systemd-tmpfiles --create -) <<EOF +F /tmp/F/ro-fs/foo 0644 - - - - +EOF + +(! systemd-tmpfiles --create -) <<EOF +F /tmp/F/ro-fs/foo 0644 - - - - This string should not be written +EOF +test -f /tmp/F/ro-fs/foo +grep -q 'truncating is not allowed' /tmp/F/ro-fs/foo + +# Trying to change the perms should fail. +: >/tmp/F/rw-fs/foo +(! systemd-tmpfiles --create -) <<EOF +F /tmp/F/ro-fs/foo 0666 - - - - +EOF +test "$(stat -c %U:%G:%a /tmp/F/ro-fs/foo)" = "root:root:644" + +### Try to create a new file. +(! systemd-tmpfiles --create -) <<EOF +F /tmp/F/ro-fs/bar 0644 - - - - +EOF +test ! -e /tmp/F/ro-fs/bar + +### 'F' shouldn't follow unsafe paths. +mkdir /tmp/F/daemon +ln -s /root /tmp/F/daemon/unsafe-symlink +chown -R --no-dereference daemon:daemon /tmp/F/daemon + +(! systemd-tmpfiles --create -) <<EOF +F /tmp/F/daemon/unsafe-symlink/exploit 0644 daemon daemon - - +EOF +test ! -e /tmp/F/daemon/unsafe-symlink/exploit + +# +# 'w' +# +touch /tmp/w/overwritten +touch /tmp/w/appended + +### nop if the target does not exist. +systemd-tmpfiles --create - <<EOF +w /tmp/w/unexistent 0644 - - - new content +EOF +test ! -e /tmp/w/unexistent + +### no argument given -> fails. +(! systemd-tmpfiles --create -) <<EOF +w /tmp/w/unexistent 0644 - - - - +EOF + +### write into an empty file. +systemd-tmpfiles --create - <<EOF +w /tmp/w/overwritten 0644 - - - old content +EOF +test -f /tmp/w/overwritten +test "$(< /tmp/w/overwritten)" = "old content" + +### old content is overwritten +systemd-tmpfiles --create - <<EOF +w /tmp/w/overwritten 0644 - - - new content +EOF +test -f /tmp/w/overwritten +test "$(< /tmp/w/overwritten)" = "new content" + +### append lines +systemd-tmpfiles --create - <<EOF +w+ /tmp/w/appended 0644 - - - 1 +w+ /tmp/w/appended 0644 - - - 2\n +w+ /tmp/w/appended 0644 - - - 3 +EOF +test -f /tmp/w/appended +test "$(< /tmp/w/appended)" = "$(echo -ne '12\n3')" + +### writing into an 'exotic' file should be allowed. +systemd-tmpfiles --create - <<EOF +w /dev/null - - - - new content +EOF + +### 'w' follows symlinks +ln -s ./overwritten /tmp/w/symlink +systemd-tmpfiles --create - <<EOF +w /tmp/w/symlink - - - - $(readlink -e /tmp/w/symlink) +EOF +readlink -e /tmp/w/symlink +test "$(< /tmp/w/overwritten)" = "/tmp/w/overwritten" + +### 'w' shouldn't follow unsafe paths. +mkdir /tmp/w/daemon +ln -s /root /tmp/w/daemon/unsafe-symlink +chown -R --no-dereference daemon:daemon /tmp/w/daemon + +(! systemd-tmpfiles --create -) <<EOF +f /tmp/w/daemon/unsafe-symlink/exploit 0644 daemon daemon - - +EOF +test ! -e /tmp/w/daemon/unsafe-symlink/exploit diff --git a/test/units/testsuite-22.04.sh b/test/units/testsuite-22.04.sh new file mode 100755 index 0000000..7bf2b28 --- /dev/null +++ b/test/units/testsuite-22.04.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Basic tests for types creating fifos +set -eux +set -o pipefail + +rm -fr /tmp/p +mkdir /tmp/p +touch /tmp/p/f1 + +systemd-tmpfiles --create - <<EOF +p /tmp/p/fifo1 0666 - - - - +EOF + +test -p /tmp/p/fifo1 +test "$(stat -c %U:%G:%a /tmp/p/fifo1)" = "root:root:666" + +# Refuse to overwrite an existing file. Error is not propagated. +systemd-tmpfiles --create - <<EOF +p /tmp/p/f1 0666 - - - - +EOF + +test -f /tmp/p/f1 + +# unless '+' prefix is used +systemd-tmpfiles --create - <<EOF +p+ /tmp/p/f1 0666 - - - - +EOF + +test -p /tmp/p/f1 +test "$(stat -c %U:%G:%a /tmp/p/f1)" = "root:root:666" + +# +# Must be fixed +# +# mkdir /tmp/p/daemon +# #ln -s /root /tmp/F/daemon/unsafe-symlink +# chown -R --no-dereference daemon:daemon /tmp/p/daemon +# +# systemd-tmpfiles --create - <<EOF +# p /tmp/p/daemon/fifo2 0666 daemon daemon - - +# EOF diff --git a/test/units/testsuite-22.05.sh b/test/units/testsuite-22.05.sh new file mode 100755 index 0000000..cde9b5d --- /dev/null +++ b/test/units/testsuite-22.05.sh @@ -0,0 +1,45 @@ +#! /bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +rm -fr /tmp/{z,Z} +mkdir /tmp/{z,Z} + +# +# 'z' +# +mkdir /tmp/z/d{1,2} +touch /tmp/z/f1 /tmp/z/d1/f11 /tmp/z/d2/f21 + +systemd-tmpfiles --create - <<EOF +z /tmp/z/f1 0755 daemon daemon - - +z /tmp/z/d1 0755 daemon daemon - - +EOF + +test "$(stat -c %U:%G /tmp/z/f1)" = "daemon:daemon" +test "$(stat -c %U:%G /tmp/z/d1)" = "daemon:daemon" +test "$(stat -c %U:%G /tmp/z/d1/f11)" = "root:root" + +systemd-tmpfiles --create - <<EOF +z /tmp/z/d2/* 0755 daemon daemon - - +EOF + +test "$(stat -c %U:%G /tmp/z/d2/f21)" = "daemon:daemon" + +# +# 'Z' +# +mkdir /tmp/Z/d1 /tmp/Z/d1/d11 +touch /tmp/Z/f1 /tmp/Z/d1/f11 /tmp/Z/d1/d11/f111 + +systemd-tmpfiles --create - <<EOF +Z /tmp/Z/f1 0755 daemon daemon - - +Z /tmp/Z/d1 0755 daemon daemon - - +EOF + +test "$(stat -c %U:%G /tmp/Z/f1)" = "daemon:daemon" +test "$(stat -c %U:%G /tmp/Z/d1)" = "daemon:daemon" +test "$(stat -c %U:%G /tmp/Z/d1/d11)" = "daemon:daemon" +test "$(stat -c %U:%G /tmp/Z/d1/f11)" = "daemon:daemon" +test "$(stat -c %U:%G /tmp/Z/d1/d11/f111)" = "daemon:daemon" diff --git a/test/units/testsuite-22.06.sh b/test/units/testsuite-22.06.sh new file mode 100755 index 0000000..f64a95c --- /dev/null +++ b/test/units/testsuite-22.06.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Inspired by https://github.com/systemd/systemd/issues/9508 +set -eux +set -o pipefail + +test_snippet() { + systemd-tmpfiles "$@" - <<EOF +d /var/tmp/foobar-test-06 +d /var/tmp/foobar-test-06/important +R /var/tmp/foobar-test-06 +EOF +} + +test_snippet --create --remove +test -d /var/tmp/foobar-test-06 +test -d /var/tmp/foobar-test-06/important + +test_snippet --remove +test ! -f /var/tmp/foobar-test-06 +test ! -f /var/tmp/foobar-test-06/important + +test_snippet --create +test -d /var/tmp/foobar-test-06 +test -d /var/tmp/foobar-test-06/important + +touch /var/tmp/foobar-test-06/something-else + +test_snippet --create +test -d /var/tmp/foobar-test-06 +test -d /var/tmp/foobar-test-06/important +test -f /var/tmp/foobar-test-06/something-else + +test_snippet --create --remove +test -d /var/tmp/foobar-test-06 +test -d /var/tmp/foobar-test-06/important +test ! -f /var/tmp/foobar-test-06/something-else diff --git a/test/units/testsuite-22.07.sh b/test/units/testsuite-22.07.sh new file mode 100755 index 0000000..de20d5e --- /dev/null +++ b/test/units/testsuite-22.07.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Verifies the issues described by https://github.com/systemd/systemd/issues/10191 +set -eux +set -o pipefail + +rm -rf /tmp/test-prefix + +mkdir /tmp/test-prefix +touch /tmp/test-prefix/file + +systemd-tmpfiles --remove - <<EOF +r /tmp/test-prefix +r /tmp/test-prefix/file +EOF + +test ! -f /tmp/test-prefix/file +test ! -f /tmp/test-prefix + +mkdir /tmp/test-prefix +touch /tmp/test-prefix/file + +systemd-tmpfiles --remove - <<EOF +r /tmp/test-prefix/file +r /tmp/test-prefix +EOF + +test ! -f /tmp/test-prefix/file +test ! -f /tmp/test-prefix diff --git a/test/units/testsuite-22.08.sh b/test/units/testsuite-22.08.sh new file mode 100755 index 0000000..40fafd3 --- /dev/null +++ b/test/units/testsuite-22.08.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Verify tmpfiles can run in a root directory under a path prefix that contains +# directories owned by unprivileged users, for example when a root file system +# is mounted in a regular user's home directory. +# +# https://github.com/systemd/systemd/pull/11820 +set -eux +set -o pipefail + +rm -fr /tmp/root /tmp/user +mkdir -p /tmp/root /tmp/user/root +chown daemon:daemon /tmp/user + +# Verify the command works as expected with no prefix or a root-owned prefix. +echo 'd /tmp/root/test1' | systemd-tmpfiles --create - +test -d /tmp/root/test1 +echo 'd /test2' | systemd-tmpfiles --root=/tmp/root --create - +test -d /tmp/root/test2 + +# Verify the command fails to write to a root-owned subdirectory under an +# unprivileged user's directory when it's not part of the prefix, as expected +# by the unsafe_transition function. +echo 'd /tmp/user/root/test' | (! systemd-tmpfiles --create -) +test ! -e /tmp/user/root/test +echo 'd /user/root/test' | (! systemd-tmpfiles --root=/tmp --create -) +test ! -e /tmp/user/root/test + +# Verify the above works when all user-owned directories are in the prefix. +echo 'd /test' | systemd-tmpfiles --root=/tmp/user/root --create - +test -d /tmp/user/root/test diff --git a/test/units/testsuite-22.09.sh b/test/units/testsuite-22.09.sh new file mode 100755 index 0000000..0857773 --- /dev/null +++ b/test/units/testsuite-22.09.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Make sure that the "stat" output is not locale dependent. +export LANG=C LC_ALL=C + +# first, create file without suid/sgid +systemd-tmpfiles --create - <<EOF +f /tmp/xxx 0755 1 1 - - +f /tmp/yyy 0755 1 1 - - +EOF + +test "$(stat -c %F:%u:%g:%a /tmp/xxx)" = "regular empty file:1:1:755" +test "$(stat -c %F:%u:%g:%a /tmp/yyy)" = "regular empty file:1:1:755" + +# then, add suid/sgid +systemd-tmpfiles --create - <<EOF +f /tmp/xxx 04755 +f /tmp/yyy 02755 +EOF + +test "$(stat -c %F:%u:%g:%a /tmp/xxx)" = "regular empty file:1:1:4755" +test "$(stat -c %F:%u:%g:%a /tmp/yyy)" = "regular empty file:1:1:2755" + +# then, chown the files to somebody else +systemd-tmpfiles --create - <<EOF +f /tmp/xxx - 2 2 +f /tmp/yyy - 2 2 +EOF + +test "$(stat -c %F:%u:%g:%a /tmp/xxx)" = "regular empty file:2:2:4755" +test "$(stat -c %F:%u:%g:%a /tmp/yyy)" = "regular empty file:2:2:2755" + +# then, chown the files to a third user/group but also drop to a mask that has +# both more and fewer bits set +systemd-tmpfiles --create - <<EOF +f /tmp/xxx 0770 3 3 +f /tmp/yyy 0770 3 3 +EOF + +test "$(stat -c %F:%u:%g:%a /tmp/xxx)" = "regular empty file:3:3:770" +test "$(stat -c %F:%u:%g:%a /tmp/yyy)" = "regular empty file:3:3:770" + +# return to the beginning +systemd-tmpfiles --create - <<EOF +f /tmp/xxx 0755 1 1 - - +f /tmp/yyy 0755 1 1 - - +EOF + +test "$(stat -c %F:%u:%g:%a /tmp/xxx)" = "regular empty file:1:1:755" +test "$(stat -c %F:%u:%g:%a /tmp/yyy)" = "regular empty file:1:1:755" + +# remove everything +systemd-tmpfiles --remove - <<EOF +r /tmp/xxx +r /tmp/yyy +EOF diff --git a/test/units/testsuite-22.10.sh b/test/units/testsuite-22.10.sh new file mode 100755 index 0000000..99052c8 --- /dev/null +++ b/test/units/testsuite-22.10.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-tmpfiles --create - <<EOF +f /tmp/xxx1 0644 - - - foo +f /tmp/xxx2 0644 - - - foo bar +f /tmp/xxx3 0644 - - - foo\x20bar +f /tmp/xxx4 0644 - - - \x20foobar +f /tmp/xxx5 0644 - - - foobar\x20 +f /tmp/xxx6 0644 - - - foo bar +f /tmp/xxx7 0644 - - - foo bar \n +f /tmp/xxx8 0644 - - - " foo bar " +f /tmp/xxx9 0644 - - - ' foo bar ' +EOF + +echo -n "foo" | cmp /tmp/xxx1 - +echo -n "foo bar" | cmp /tmp/xxx2 - +echo -n "foo bar" | cmp /tmp/xxx3 - +echo -n " foobar" | cmp /tmp/xxx4 - +echo -n "foobar " | cmp /tmp/xxx5 - +echo -n "foo bar" | cmp /tmp/xxx6 - +echo "foo bar " | cmp /tmp/xxx7 - +echo -n "\" foo bar \"" | cmp /tmp/xxx8 - +echo -n "' foo bar '" | cmp /tmp/xxx9 - + +rm /tmp/xxx{1,2,3,4,5,6,7,8,9} diff --git a/test/units/testsuite-22.11.sh b/test/units/testsuite-22.11.sh new file mode 100755 index 0000000..f71a95f --- /dev/null +++ b/test/units/testsuite-22.11.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -e +set -x + +rm -fr /tmp/x +mkdir /tmp/x + +# +# 'x' +# +mkdir -p /tmp/x/{1,2} +touch /tmp/x/1/{x1,x2} /tmp/x/2/{y1,y2} /tmp/x/{z1,z2} + +systemd-tmpfiles --clean - <<EOF +d /tmp/x - - - 0 +x /tmp/x/1 +EOF + +find /tmp/x | sort +test -d /tmp/x/1 +test -f /tmp/x/1/x1 +test -f /tmp/x/1/x2 +test ! -d /tmp/x/2 +test ! -f /tmp/x/2/x1 +test ! -f /tmp/x/2/x2 +test ! -f /tmp/x/z1 +test ! -f /tmp/x/z2 + +# +# 'X' +# + +mkdir -p /tmp/x/{1,2} +touch /tmp/x/1/{x1,x2} /tmp/x/2/{y1,y2} /tmp/x/{z1,z2} + +systemd-tmpfiles --clean - <<EOF +d /tmp/x - - - 0 +X /tmp/x/1 +EOF + +find /tmp/x | sort +test -d /tmp/x/1 +test ! -f /tmp/x/1/x1 +test ! -f /tmp/x/1/x2 +test ! -d /tmp/x/2 +test ! -f /tmp/x/2/x1 +test ! -f /tmp/x/2/x2 +test ! -f /tmp/x/z1 +test ! -f /tmp/x/z2 + +# +# 'x' with glob +# + +mkdir -p /tmp/x/{1,2} +touch /tmp/x/1/{x1,x2} /tmp/x/2/{y1,y2} /tmp/x/{z1,z2} + +systemd-tmpfiles --clean - <<EOF +d /tmp/x - - - 0 +x /tmp/x/[1345] +x /tmp/x/z* +EOF + +find /tmp/x | sort +test -d /tmp/x/1 +test -f /tmp/x/1/x1 +test -f /tmp/x/1/x2 +test ! -d /tmp/x/2 +test ! -f /tmp/x/2/x1 +test ! -f /tmp/x/2/x2 +test -f /tmp/x/z1 +test -f /tmp/x/z2 + +# +# 'X' with glob +# + +mkdir -p /tmp/x/{1,2} +touch /tmp/x/1/{x1,x2} /tmp/x/2/{y1,y2} /tmp/x/{z1,z2} + +systemd-tmpfiles --clean - <<EOF +d /tmp/x - - - 0 +X /tmp/x/[1345] +X /tmp/x/?[12] +EOF + +find /tmp/x | sort +test -d /tmp/x/1 +test ! -f /tmp/x/1/x1 +test ! -f /tmp/x/1/x2 +test ! -d /tmp/x/2 +test ! -f /tmp/x/2/x1 +test ! -f /tmp/x/2/x2 +test -f /tmp/x/z1 +test -f /tmp/x/z2 + +# +# 'x' with 'r' +# + +mkdir -p /tmp/x/{1,2}/a +touch /tmp/x/1/a/{x1,x2} /tmp/x/2/a/{y1,y2} + +systemd-tmpfiles --clean - <<EOF +# x/X is not supposed to influence r +x /tmp/x/1/a +X /tmp/x/2/a +r /tmp/x/1 +r /tmp/x/2 +EOF + +find /tmp/x | sort +test -d /tmp/x/1 +test -d /tmp/x/1/a +test -f /tmp/x/1/a/x1 +test -f /tmp/x/1/a/x2 +test -f /tmp/x/2/a/y1 +test -f /tmp/x/2/a/y2 + +# +# 'x' with 'R' +# + +mkdir -p /tmp/x/{1,2}/a +touch /tmp/x/1/a/{x1,x2} /tmp/x/2/a/{y1,y2} + +systemd-tmpfiles --remove - <<EOF +# X is not supposed to influence R +X /tmp/x/1/a +X /tmp/x/2/a +R /tmp/x/1 +EOF + +find /tmp/x | sort +test ! -d /tmp/x/1 +test ! -d /tmp/x/1/a +test ! -f /tmp/x/1/a/x1 +test ! -f /tmp/x/1/a/x2 +test -f /tmp/x/2/a/y1 +test -f /tmp/x/2/a/y2 diff --git a/test/units/testsuite-22.12.sh b/test/units/testsuite-22.12.sh new file mode 100755 index 0000000..b8c4da8 --- /dev/null +++ b/test/units/testsuite-22.12.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Test the "Age" parameter (with age-by) for systemd-tmpfiles. +set -e +set -x + +# Test directory structure looks like this: +# /tmp/ageby/ +# ├── d1 +# │ ├── f1 +# │ ├── f2 +# │ ├── f3 +# │ └── f4 +# ├── d2 +# │ ├── f1 +# │ ├── f2 +# ... + +export SYSTEMD_LOG_LEVEL="debug" + +rm -rf /tmp/ageby +mkdir -p /tmp/ageby/d{1..4} + +# TODO: There is probably a better way to figure this out. +# Test for [bB] age-by arguments only on filesystems that expose +# the creation time. Note that this is _not_ an accurate way to +# check if the filesystem or kernel version don't provide the +# timestamp. But, if the timestamp is visible in "stat" it is a +# good indicator that the test can be run. +TEST_TMPFILES_AGEBY_BTIME=${TEST_TMPFILES_AGEBY_BTIME:-0} +if stat --format "%w" /tmp/ageby 2>/dev/null | grep -qv '^[\?\-]$'; then + TEST_TMPFILES_AGEBY_BTIME=1 +fi + +touch -a --date "2 minutes ago" /tmp/ageby/d1/f1 +touch -m --date "4 minutes ago" /tmp/ageby/d2/f1 + +# Create a bunch of other files. +touch /tmp/ageby/d{1,2}/f{2..4} + +# For "ctime". +touch /tmp/ageby/d3/f1 +chmod +x /tmp/ageby/d3/f1 +sleep 1 + +# For "btime". +touch /tmp/ageby/d4/f1 +sleep 1 + +# More files with recent "{a,b}time" values. +touch /tmp/ageby/d{3,4}/f{2..4} + +# Check for cleanup of "f1" in each of "/tmp/d{1..4}". +systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby/d1 - - - a:1m - +e /tmp/ageby/d2 - - - m:3m - +D /tmp/ageby/d3 - - - c:2s - +EOF + +for d in d{1..3}; do + test ! -f "/tmp/ageby/${d}/f1" +done + +if [[ $TEST_TMPFILES_AGEBY_BTIME -gt 0 ]]; then + systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby/d4 - - - b:1s - +EOF + + test ! -f "/tmp/ageby/d4/f1" +else + # Remove the file manually. + rm "/tmp/ageby/d4/f1" +fi + +# Check for an invalid "age" and "age-by" arguments. +for a in ':' ':1s' '2:1h' 'nope:42h' '" :7m"' 'm:' '::' '"+r^w-x:2/h"' 'b ar::64'; do + systemd-tmpfiles --clean - <<EOF 2>&1 | grep -q -F 'Invalid age' +d /tmp/ageby - - - ${a} - +EOF +done + +for d in d{1..4}; do + for f in f{2..4}; do + test -f "/tmp/ageby/${d}/${f}" + done +done + +# Check for parsing with whitespace, repeated values +# for "age-by" (valid arguments). +for a in '" a:24h"' 'cccaab:2h' '" aa : 4h"' '" a A B C c:1h"'; do + systemd-tmpfiles --clean - <<EOF +d /tmp/ageby - - - ${a} - +EOF +done + +for d in d{1..4}; do + for f in f{2..4}; do + test -f "/tmp/ageby/${d}/${f}" + done +done + +# Check that all files are removed if the "Age" is +# set to "0" (regardless of "age-by" argument). +systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby/d1 - - - abc:0 - +e /tmp/ageby/d2 - - - cmb:0 - +EOF + +for d in d{1,2}; do + for f in f{2..4}; do + test ! -f "/tmp/ageby/${d}/${f}" + done +done + +# Check for combinations: +# - "/tmp/ageby/d3/f2" has file timestamps that +# are older than the specified age, it will be +# removed +# - "/tmp/ageby/d4/f2", has not aged for the given +# timestamp combination, it will not be removed +touch -a -m --date "4 minutes ago" /tmp/ageby/d3/f2 +touch -a -m --date "8 minutes ago" /tmp/ageby/d4/f2 +systemd-tmpfiles --clean - <<-EOF +e /tmp/ageby/d3 - - - am:3m - +D /tmp/ageby/d4 - - - mc:7m - +EOF + +test ! -f "/tmp/ageby/d3/f2" +test -f "/tmp/ageby/d4/f2" + +# Check that all files are removed if only "Age" is set to 0. +systemd-tmpfiles --clean - <<-EOF +e /tmp/ageby/d3 - - - 0s +d /tmp/ageby/d4 - - - 0s +EOF + +for d in d{3,4}; do + for f in f{2..4}; do + test ! -f "/tmp/ageby/$d/${f}" + done +done + +# Check "age-by" argument for sub-directories in "/tmp/ageby". +systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby/ - - - A:1m - +EOF + +for d in d{1..4}; do + test -d "/tmp/ageby/${d}" +done + +# Check for combinations. +touch -a -m --date "5 seconds ago" /tmp/ageby/d{1,2} +systemd-tmpfiles --clean - <<-EOF +e /tmp/ageby/ - - - AM:4s - +EOF + +for d in d{1,2}; do + test ! -d "/tmp/ageby/${d}" +done + +for d in d{3,4}; do + test -d "/tmp/ageby/${d}" +done + +# Check "btime" for directories. +if [[ $TEST_TMPFILES_AGEBY_BTIME -gt 0 ]]; then + systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby/ - - - B:8s - +EOF + + for d in d{3,4}; do + test -d "/tmp/ageby/${d}" + done +fi + +# To bump "atime". +touch -a --date "1 second ago" /tmp/ageby/d3 +systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby/ - - - A:2s - +EOF + +test -d /tmp/ageby/d3 +test ! -d /tmp/ageby/d4 + +# Check if sub-directories are removed regardless +# of "age-by", when "Age" is set to "0". +systemd-tmpfiles --clean - <<-EOF +D /tmp/ageby/ - - - AM:0 - +EOF + +test ! -d /tmp/ageby/d3 + +# Cleanup the test directory (fail if not empty). +rmdir /tmp/ageby diff --git a/test/units/testsuite-22.13.sh b/test/units/testsuite-22.13.sh new file mode 100755 index 0000000..33ef451 --- /dev/null +++ b/test/units/testsuite-22.13.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Tests for configuration directory and file precedences +# +set -eux + +rm -f /{usr/lib,etc}/tmpfiles.d/{L,w}-*.conf +rm -fr /tmp/precedence/{L,w} + +mkdir -p /{usr/lib,etc}/tmpfiles.d +mkdir -p /tmp/precedence/{L,w} + +# +# 'L' +# +ln -s /dev/null /tmp/precedence/L + +# Overwrite the existing symlink +cat >/usr/lib/tmpfiles.d/L-z.conf<<EOF +L+ /tmp/precedence/L - - - - /usr/lib/tmpfiles.d/L-z.conf +EOF + +systemd-tmpfiles --create +test "$(readlink /tmp/precedence/L)" = "/usr/lib/tmpfiles.d/L-z.conf" + +# Files in /etc should override those in /usr +cat >/etc/tmpfiles.d/L-z.conf<<EOF +L+ /tmp/precedence/L - - - - /etc/tmpfiles.d/L-z.conf +EOF + +systemd-tmpfiles --create +test "$(readlink /tmp/precedence/L)" = "/etc/tmpfiles.d/L-z.conf" + +# /usr/…/L-a.conf has higher prio than /etc/…/L-z.conf +cat >/usr/lib/tmpfiles.d/L-a.conf<<EOF +L+ /tmp/precedence/L - - - - /usr/lib/tmpfiles.d/L-a.conf +EOF + +systemd-tmpfiles --create +test "$(readlink /tmp/precedence/L)" = "/usr/lib/tmpfiles.d/L-a.conf" + +# Files in /etc should override those in /usr +cat >/etc/tmpfiles.d/L-a.conf<<EOF +L+ /tmp/precedence/L - - - - /etc/tmpfiles.d/L-a.conf +EOF + +systemd-tmpfiles --create +test "$(readlink /tmp/precedence/L)" = "/etc/tmpfiles.d/L-a.conf" + +# +# 'w' +# +touch /tmp/precedence/w/f + +# Multiple configuration files specifying 'w+' for the same path is allowed. +for i in a c; do + cat >/usr/lib/tmpfiles.d/w-$i.conf<<EOF +w+ /tmp/precedence/w/f - - - - /usr/lib/tmpfiles.d/w-$i.conf\n +EOF + cat >/etc/tmpfiles.d/w-$i.conf<<EOF +w+ /tmp/precedence/w/f - - - - /etc/tmpfiles.d/w-$i.conf\n +EOF +done + +cat >/usr/lib/tmpfiles.d/w-b.conf<<EOF +w+ /tmp/precedence/w/f - - - - /usr/lib/tmpfiles.d/w-b.conf\n +EOF + +systemd-tmpfiles --create +cmp /tmp/precedence/w/f <<EOF +/etc/tmpfiles.d/w-a.conf +/usr/lib/tmpfiles.d/w-b.conf +/etc/tmpfiles.d/w-c.conf +EOF diff --git a/test/units/testsuite-22.14.sh b/test/units/testsuite-22.14.sh new file mode 100755 index 0000000..2132de7 --- /dev/null +++ b/test/units/testsuite-22.14.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Tests for the ":" uid/gid/mode modifier +# +set -eux + +rm -rf /tmp/someinode + +systemd-tmpfiles --create - <<EOF +d /tmp/someinode :0123 :1 :1 +EOF +test "$(stat -c %F:%u:%g:%a /tmp/someinode)" = "directory:1:1:123" + +systemd-tmpfiles --create - <<EOF +d /tmp/someinode :0321 :2 :2 +EOF +test "$(stat -c %F:%u:%g:%a /tmp/someinode)" = "directory:1:1:123" + +systemd-tmpfiles --create - <<EOF +d /tmp/someinode 0321 2 2 +EOF +test "$(stat -c %F:%u:%g:%a /tmp/someinode)" = "directory:2:2:321" + +systemd-tmpfiles --create - <<EOF +d /tmp/someinode :0123 :1 :1 +EOF +test "$(stat -c %F:%u:%g:%a /tmp/someinode)" = "directory:2:2:321" + +rm -rf /tmp/someinode + +systemd-tmpfiles --create - <<EOF +d /tmp/someinode :0123 :1 :1 +EOF +test "$(stat -c %F:%u:%g:%a /tmp/someinode)" = "directory:1:1:123" + +rm -rf /tmp/someinode diff --git a/test/units/testsuite-22.15.sh b/test/units/testsuite-22.15.sh new file mode 100755 index 0000000..6cbb498 --- /dev/null +++ b/test/units/testsuite-22.15.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Check specifier expansion in L lines. +# +set -eux + +rm -fr /tmp/L +mkdir /tmp/L + +# Check that %h expands to $home. +home='/somewhere' +dst='/tmp/L/1' +src="$home" +HOME="$home" \ +systemd-tmpfiles --create - <<EOF +L $dst - - - - %h +EOF +test "$(readlink "$dst")" = "$src" + +# Check that %h in the path is expanded, but +# the result of this expansion is not expanded once again. +root='/tmp/L/2' +home='/%U' +src="/usr/share/factory$home" +mkdir -p "$root$src" +dst="$root$home" +HOME="$home" \ +systemd-tmpfiles --create --root="$root" - <<EOF +L %h - - - - +EOF +test "$(readlink "$dst")" = "$src" diff --git a/test/units/testsuite-22.16.sh b/test/units/testsuite-22.16.sh new file mode 100755 index 0000000..555e07f --- /dev/null +++ b/test/units/testsuite-22.16.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Test for conditionalized execute bit ('X' bit) +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +rm -f /tmp/acl_exec +touch /tmp/acl_exec + +# No ACL set yet +systemd-tmpfiles --create - <<EOF +a /tmp/acl_exec - - - - u:root:rwX +EOF +assert_in 'user:root:rw-' "$(getfacl -Ec /tmp/acl_exec)" + +# Set another ACL and append +setfacl -m g:root:x /tmp/acl_exec + +systemd-tmpfiles --create - <<EOF +a+ /tmp/acl_exec - - - - u:root:rwX +EOF +acl="$(getfacl -Ec /tmp/acl_exec)" +assert_in 'user:root:rwx' "$acl" +assert_in 'group:root:--x' "$acl" + +# Reset ACL (no append) +systemd-tmpfiles --create - <<EOF +a /tmp/acl_exec - - - - u:root:rwX +EOF +assert_in 'user:root:rw-' "$(getfacl -Ec /tmp/acl_exec)" + +rm -f /tmp/acl_exec diff --git a/test/units/testsuite-22.17.sh b/test/units/testsuite-22.17.sh new file mode 100755 index 0000000..f43aba5 --- /dev/null +++ b/test/units/testsuite-22.17.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Test for C-style escapes in file names and contents +set -eux +set -o pipefail + +data="\x20foo\nbar" +dst="/tmp/x/\x20a\nb" + +systemd-tmpfiles --create - <<EOF +f "$dst" 0644 0 0 - $data +EOF + +diff "$(printf "/tmp/x/\x20a\nb")" <(printf "\x20foo\nbar") diff --git a/test/units/testsuite-22.service b/test/units/testsuite-22.service new file mode 100644 index 0000000..a5ed660 --- /dev/null +++ b/test/units/testsuite-22.service @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-22-TMPFILES +After=systemd-tmpfiles-setup.service +Before=getty-pre.target +Wants=getty-pre.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-22.sh b/test/units/testsuite-22.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-22.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-23-short-lived.sh b/test/units/testsuite-23-short-lived.sh new file mode 100755 index 0000000..4a12c7f --- /dev/null +++ b/test/units/testsuite-23-short-lived.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex + +if [ -f /tmp/testsuite-23.counter ] ; then + read -r counter < /tmp/testsuite-23.counter + counter=$((counter + 1)) +else + counter=0 +fi + +echo "$counter" >/tmp/testsuite-23.counter + +if [ "$counter" -eq 5 ] ; then + systemctl kill --kill-whom=main -sUSR1 testsuite-23.service +fi + +exec sleep 1.5 diff --git a/test/units/testsuite-23.ExecReload.sh b/test/units/testsuite-23.ExecReload.sh new file mode 100755 index 0000000..b497f73 --- /dev/null +++ b/test/units/testsuite-23.ExecReload.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test ExecReload= (PR #13098) + +systemd-analyze log-level debug + +export SYSTEMD_PAGER= +SERVICE_PATH="$(mktemp /etc/systemd/system/execreloadXXX.service)" +SERVICE_NAME="${SERVICE_PATH##*/}" + +echo "[#1] Failing ExecReload= should not kill the service" +cat >"$SERVICE_PATH" <<EOF +[Service] +ExecStart=/bin/sleep infinity +ExecReload=/bin/false +EOF + +systemctl daemon-reload +systemctl start "$SERVICE_NAME" +systemctl status "$SERVICE_NAME" +# The reload SHOULD fail but SHOULD NOT affect the service state +(! systemctl reload "$SERVICE_NAME") +systemctl status "$SERVICE_NAME" +systemctl stop "$SERVICE_NAME" + + +echo "[#2] Failing ExecReload= should not kill the service (multiple ExecReload=)" +cat >"$SERVICE_PATH" <<EOF +[Service] +ExecStart=/bin/sleep infinity +ExecReload=/bin/true +ExecReload=/bin/false +ExecReload=/bin/true +EOF + +systemctl daemon-reload +systemctl start "$SERVICE_NAME" +systemctl status "$SERVICE_NAME" +# The reload SHOULD fail but SHOULD NOT affect the service state +(! systemctl reload "$SERVICE_NAME") +systemctl status "$SERVICE_NAME" +systemctl stop "$SERVICE_NAME" + +echo "[#3] Failing ExecReload=- should not affect reload's exit code" +cat >"$SERVICE_PATH" <<EOF +[Service] +ExecStart=/bin/sleep infinity +ExecReload=-/bin/false +EOF + +systemctl daemon-reload +systemctl start "$SERVICE_NAME" +systemctl status "$SERVICE_NAME" +systemctl reload "$SERVICE_NAME" +systemctl status "$SERVICE_NAME" +systemctl stop "$SERVICE_NAME" + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.ExecStopPost.sh b/test/units/testsuite-23.ExecStopPost.sh new file mode 100755 index 0000000..aeaf3aa --- /dev/null +++ b/test/units/testsuite-23.ExecStopPost.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux + +# Test that ExecStopPost= is always run + +systemd-analyze log-level debug + +systemd-run --unit=simple1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=simple \ + -p ExecStopPost='/bin/touch /run/simple1' true +test -f /run/simple1 + +(! systemd-run --unit=simple2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=simple \ + -p ExecStopPost='/bin/touch /run/simple2' false) +test -f /run/simple2 + +systemd-run --unit=exec1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=exec \ + -p ExecStopPost='/bin/touch /run/exec1' sleep 1 +test -f /run/exec1 + +(! systemd-run --unit=exec2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=exec \ + -p ExecStopPost='/bin/touch /run/exec2' sh -c 'sleep 1; false') +test -f /run/exec2 + +cat >/tmp/forking1.sh <<EOF +#!/usr/bin/env bash + +set -eux + +sleep 4 & +MAINPID=\$! +disown + +systemd-notify MAINPID=\$MAINPID +EOF +chmod +x /tmp/forking1.sh + +systemd-run --unit=forking1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=forking -p NotifyAccess=exec \ + -p ExecStopPost='/bin/touch /run/forking1' /tmp/forking1.sh +test -f /run/forking1 + +cat >/tmp/forking2.sh <<EOF +#!/usr/bin/env bash + +set -eux + +(sleep 4; exit 1) & +MAINPID=\$! +disown + +systemd-notify MAINPID=\$MAINPID +EOF +chmod +x /tmp/forking2.sh + +(! systemd-run --unit=forking2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=forking -p NotifyAccess=exec \ + -p ExecStopPost='/bin/touch /run/forking2' /tmp/forking2.sh) +test -f /run/forking2 + +systemd-run --unit=oneshot1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=oneshot \ + -p ExecStopPost='/bin/touch /run/oneshot1' true +test -f /run/oneshot1 + +(! systemd-run --unit=oneshot2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=oneshot \ + -p ExecStopPost='/bin/touch /run/oneshot2' false) +test -f /run/oneshot2 + +systemd-run --unit=dbus1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=dbus -p BusName=systemd.test.ExecStopPost \ + -p ExecStopPost='/bin/touch /run/dbus1' \ + busctl call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus RequestName su systemd.test.ExecStopPost 4 || : +test -f /run/dbus1 + +systemd-run --unit=dbus2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=dbus -p BusName=systemd.test.ExecStopPost \ + -p ExecStopPost='/bin/touch /run/dbus2' true +test -f /run/dbus2 + +# https://github.com/systemd/systemd/issues/19920 +(! systemd-run --unit=dbus3.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=dbus \ + -p ExecStopPost='/bin/touch /run/dbus3' true) + +cat >/tmp/notify1.sh <<EOF +#!/usr/bin/env bash + +set -eux + +systemd-notify --ready +EOF +chmod +x /tmp/notify1.sh + +systemd-run --unit=notify1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=notify \ + -p ExecStopPost='/bin/touch /run/notify1' /tmp/notify1.sh +test -f /run/notify1 + +(! systemd-run --unit=notify2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=notify \ + -p ExecStopPost='/bin/touch /run/notify2' true) +test -f /run/notify2 + +systemd-run --unit=idle1.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=idle -p ExecStopPost='/bin/touch /run/idle1' true +test -f /run/idle1 + +(! systemd-run --unit=idle2.service --wait -p StandardOutput=tty -p StandardError=tty -p Type=idle \ + -p ExecStopPost='/bin/touch /run/idle2' false) +test -f /run/idle2 + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.JoinsNamespaceOf.sh b/test/units/testsuite-23.JoinsNamespaceOf.sh new file mode 100755 index 0000000..68ba465 --- /dev/null +++ b/test/units/testsuite-23.JoinsNamespaceOf.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +set -eux +set -o pipefail + +# Test JoinsNamespaceOf= with PrivateTmp=yes + +systemd-analyze log-level debug +systemd-analyze log-target journal + +# simple case +systemctl start testsuite-23-joins-namespace-of-1.service +systemctl start testsuite-23-joins-namespace-of-2.service +systemctl start testsuite-23-joins-namespace-of-3.service +systemctl stop testsuite-23-joins-namespace-of-1.service + +# inverse dependency +systemctl start testsuite-23-joins-namespace-of-4.service +systemctl start testsuite-23-joins-namespace-of-5.service +systemctl stop testsuite-23-joins-namespace-of-4.service + +# transitive dependency +systemctl start testsuite-23-joins-namespace-of-6.service +systemctl start testsuite-23-joins-namespace-of-7.service +systemctl start testsuite-23-joins-namespace-of-8.service +systemctl start testsuite-23-joins-namespace-of-9.service +systemctl stop testsuite-23-joins-namespace-of-6.service +systemctl stop testsuite-23-joins-namespace-of-8.service + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.RuntimeDirectoryPreserve.sh b/test/units/testsuite-23.RuntimeDirectoryPreserve.sh new file mode 100755 index 0000000..ca57702 --- /dev/null +++ b/test/units/testsuite-23.RuntimeDirectoryPreserve.sh @@ -0,0 +1,26 @@ +#!/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 +set -eux +set -o pipefail + +# Test RuntimeDirectoryPreserve=yes + +at_exit() { + set +e + + rm -fr /run/hoge /tmp/aaa +} + +trap at_exit EXIT + +systemd-mount -p RuntimeDirectory=hoge -p RuntimeDirectoryPreserve=yes -t tmpfs tmpfs /tmp/aaa + +touch /run/hoge/foo +touch /tmp/aaa/bbb + +systemctl restart tmp-aaa.mount + +test -e /run/hoge/foo +test ! -e /tmp/aaa/bbb diff --git a/test/units/testsuite-23.StandardOutput.sh b/test/units/testsuite-23.StandardOutput.sh new file mode 100755 index 0000000..50b9ac2 --- /dev/null +++ b/test/units/testsuite-23.StandardOutput.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test StandardOutput=file: + +systemd-analyze log-level debug + +systemd-run --wait --unit=testsuite-23-standard-output-one \ + -p StandardOutput=file:/tmp/stdout \ + -p StandardError=file:/tmp/stderr \ + -p Type=exec \ + sh -c 'echo x ; echo y >&2' +cmp /tmp/stdout <<EOF +x +EOF +cmp /tmp/stderr <<EOF +y +EOF + +systemd-run --wait --unit=testsuite-23-standard-output-two \ + -p StandardOutput=file:/tmp/stdout \ + -p StandardError=file:/tmp/stderr \ + -p Type=exec \ + sh -c 'echo z ; echo a >&2' +cmp /tmp/stdout <<EOF +z +EOF +cmp /tmp/stderr <<EOF +a +EOF + +systemd-run --wait --unit=testsuite-23-standard-output-three \ + -p StandardOutput=append:/tmp/stdout \ + -p StandardError=append:/tmp/stderr \ + -p Type=exec \ + sh -c 'echo b ; echo c >&2' +cmp /tmp/stdout <<EOF +z +b +EOF +cmp /tmp/stderr <<EOF +a +c +EOF + +systemd-run --wait --unit=testsuite-23-standard-output-four \ + -p StandardOutput=truncate:/tmp/stdout \ + -p StandardError=truncate:/tmp/stderr \ + -p Type=exec \ + sh -c 'echo a ; echo b >&2' +cmp /tmp/stdout <<EOF +a +EOF +cmp /tmp/stderr <<EOF +b +EOF + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.Upholds.sh b/test/units/testsuite-23.Upholds.sh new file mode 100755 index 0000000..e62f9c6 --- /dev/null +++ b/test/units/testsuite-23.Upholds.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +set -eux +set -o pipefail + +# Test OnSuccess= + Uphold= + PropagatesStopTo= + BindsTo= + +systemd-analyze log-level debug +systemd-analyze log-target journal + +# Idea is this: +# 1. we start testsuite-23-success.service +# 2. which through OnSuccess= starts testsuite-23-fail.service, +# 3. which through OnFailure= starts testsuite-23-uphold.service, +# 4. which through Uphold= starts/keeps testsuite-23-short-lived.service running, +# 5. which will sleep 1s when invoked, and on the 5th invocation send us a SIGUSR1 +# 6. once we got that we finish cleanly + +sigusr1=0 +trap sigusr1=1 SIGUSR1 + +trap -p SIGUSR1 + +systemctl start testsuite-23-success.service + +while [ "$sigusr1" -eq 0 ] ; do + sleep .5 +done + +systemctl stop testsuite-23-uphold.service + +systemctl enable testsuite-23-upheldby-install.service + +# Idea is this: +# 1. we start testsuite-23-retry-uphold.service +# 2. which through Uphold= starts testsuite-23-retry-upheld.service +# 3. which through Requires= starts testsuite-23-retry-fail.service +# 4. which fails as /tmp/testsuite-23-retry-fail does not exist, so testsuite-23-retry-upheld.service +# is no longer restarted +# 5. we create /tmp/testsuite-23-retry-fail +# 6. now testsuite-23-retry-upheld.service will be restarted since upheld, and its dependency will +# be satisfied + +rm -f /tmp/testsuite-23-retry-fail +systemctl start testsuite-23-retry-uphold.service +systemctl is-active testsuite-23-upheldby-install.service + +until systemctl is-failed testsuite-23-retry-fail.service ; do + sleep .5 +done + +(! systemctl is-active testsuite-23-retry-upheld.service) + +touch /tmp/testsuite-23-retry-fail + +until systemctl is-active testsuite-23-retry-upheld.service ; do + sleep .5 +done + +systemctl stop testsuite-23-retry-uphold.service testsuite-23-retry-fail.service testsuite-23-retry-upheld.service + +# Idea is this: +# 1. we start testsuite-23-prop-stop-one.service +# 2. which through Wants=/After= pulls in testsuite-23-prop-stop-two.service as well +# 3. testsuite-23-prop-stop-one.service then sleeps indefinitely +# 4. testsuite-23-prop-stop-two.service sleeps a short time and exits +# 5. the StopPropagatedFrom= dependency between the two should ensure *both* will exit as result +# 6. an ExecStopPost= line on testsuite-23-prop-stop-one.service will send us a SIGUSR2 +# 7. once we got that we finish cleanly + +sigusr2=0 +trap sigusr2=1 SIGUSR2 + +systemctl start testsuite-23-prop-stop-one.service + +while [ "$sigusr2" -eq 0 ] ; do + sleep .5 +done + + +# Idea is this: +# 1. we start testsuite-23-binds-to.service +# 2. which through BindsTo=/After= pulls in testsuite-23-bound-by.service as well +# 3. testsuite-23-bound-by.service suddenly dies +# 4. testsuite-23-binds-to.service should then also be pulled down (it otherwise just hangs) +# 6. an ExecStopPost= line on testsuite-23-binds-to.service will send us a SIGRTMIN1+1 +# 7. once we got that we finish cleanly + +sigrtmin1=0 +trap sigrtmin1=1 SIGRTMIN+1 + +systemctl start testsuite-23-binds-to.service + +while [ "$sigrtmin1" -eq 0 ] ; do + sleep .5 +done + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.clean-unit.sh b/test/units/testsuite-23.clean-unit.sh new file mode 100755 index 0000000..a82b54f --- /dev/null +++ b/test/units/testsuite-23.clean-unit.sh @@ -0,0 +1,329 @@ +#!/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 +set -eux +set -o pipefail + +# Test unit configuration/state/cache/log/runtime data cleanup + +at_exit() { + set +e + + rm -fr /{etc,run,var/lib,var/cache,var/log}/test-service + rm -fr /{etc,run,var/lib,var/cache,var/log}/private/test-service + rm -fr /{etc,run,var/lib,var/cache,var/log}/hoge + rm -fr /{etc,run,var/lib,var/cache,var/log}/test-socket +} + +trap at_exit EXIT + +cat >/run/systemd/system/test-service.service <<EOF +[Service] +ConfigurationDirectory=test-service +RuntimeDirectory=test-service +StateDirectory=test-service +CacheDirectory=test-service +LogsDirectory=test-service +RuntimeDirectoryPreserve=yes +ExecStart=/bin/sleep infinity +Type=exec +EOF + +systemctl daemon-reload + +test ! -e /etc/test-service +test ! -e /run/test-service +test ! -e /var/lib/test-service +test ! -e /var/cache/test-service +test ! -e /var/log/test-service + +systemctl start test-service + +test -d /etc/test-service +test -d /run/test-service +test -d /var/lib/test-service +test -d /var/cache/test-service +test -d /var/log/test-service + +(! systemctl clean test-service) + +systemctl stop test-service + +test -d /etc/test-service +test -d /run/test-service +test -d /var/lib/test-service +test -d /var/cache/test-service +test -d /var/log/test-service + +systemctl clean test-service --what=configuration + +test ! -e /etc/test-service +test -d /run/test-service +test -d /var/lib/test-service +test -d /var/cache/test-service +test -d /var/log/test-service + +systemctl clean test-service + +test ! -e /etc/test-service +test ! -e /run/test-service +test -d /var/lib/test-service +test ! -e /var/cache/test-service +test -d /var/log/test-service + +systemctl clean test-service --what=logs + +test ! -e /etc/test-service +test ! -e /run/test-service +test -d /var/lib/test-service +test ! -e /var/cache/test-service +test ! -e /var/log/test-service + +systemctl clean test-service --what=all + +test ! -e /etc/test-service +test ! -e /run/test-service +test ! -e /var/lib/test-service +test ! -e /var/cache/test-service +test ! -e /var/log/test-service + +cat >/run/systemd/system/test-service.service <<EOF +[Service] +DynamicUser=yes +ConfigurationDirectory=test-service +RuntimeDirectory=test-service +StateDirectory=test-service +CacheDirectory=test-service +LogsDirectory=test-service +RuntimeDirectoryPreserve=yes +ExecStart=/bin/sleep infinity +Type=exec +EOF + +systemctl daemon-reload + +test ! -e /etc/test-service +test ! -e /run/test-service +test ! -e /var/lib/test-service +test ! -e /var/cache/test-service +test ! -e /var/log/test-service + +systemctl restart test-service + +test -d /etc/test-service +test -d /run/private/test-service +test -d /var/lib/private/test-service +test -d /var/cache/private/test-service +test -d /var/log/private/test-service +test -L /run/test-service +test -L /var/lib/test-service +test -L /var/cache/test-service +test -L /var/log/test-service + +(! systemctl clean test-service) + +systemctl stop test-service + +test -d /etc/test-service +test -d /run/private/test-service +test -d /var/lib/private/test-service +test -d /var/cache/private/test-service +test -d /var/log/private/test-service +test -L /run/test-service +test -L /var/lib/test-service +test -L /var/cache/test-service +test -L /var/log/test-service + +systemctl clean test-service --what=configuration + +test ! -d /etc/test-service +test -d /run/private/test-service +test -d /var/lib/private/test-service +test -d /var/cache/private/test-service +test -d /var/log/private/test-service +test -L /run/test-service +test -L /var/lib/test-service +test -L /var/cache/test-service +test -L /var/log/test-service + +systemctl clean test-service + +test ! -d /etc/test-service +test ! -d /run/private/test-service +test -d /var/lib/private/test-service +test ! -d /var/cache/private/test-service +test -d /var/log/private/test-service +test ! -L /run/test-service +test -L /var/lib/test-service +test ! -L /var/cache/test-service +test -L /var/log/test-service + +systemctl clean test-service --what=logs + +test ! -d /etc/test-service +test ! -d /run/private/test-service +test -d /var/lib/private/test-service +test ! -d /var/cache/private/test-service +test ! -d /var/log/private/test-service +test ! -L /run/test-service +test -L /var/lib/test-service +test ! -L /var/cache/test-service +test ! -L /var/log/test-service + +systemctl clean test-service --what=all + +test ! -d /etc/test-service +test ! -d /run/private/test-service +test ! -d /var/lib/private/test-service +test ! -d /var/cache/private/test-service +test ! -d /var/log/private/test-service +test ! -L /run/test-service +test ! -L /var/lib/test-service +test ! -L /var/cache/test-service +test ! -L /var/log/test-service + +cat >/run/systemd/system/tmp-hoge.mount <<EOF +[Mount] +What=tmpfs +Type=tmpfs +ConfigurationDirectory=hoge +RuntimeDirectory=hoge +StateDirectory=hoge +CacheDirectory=hoge +LogsDirectory=hoge +EOF + +systemctl daemon-reload + +test ! -e /etc/hoge +test ! -e /run/hoge +test ! -e /var/lib/hoge +test ! -e /var/cache/hoge +test ! -e /var/log/hoge + +systemctl start tmp-hoge.mount + +test -d /etc/hoge +test -d /run/hoge +test -d /var/lib/hoge +test -d /var/cache/hoge +test -d /var/log/hoge + +(! systemctl clean tmp-hoge.mount) + +test -d /etc/hoge +test -d /run/hoge +test -d /var/lib/hoge +test -d /var/cache/hoge +test -d /var/log/hoge + +systemctl stop tmp-hoge.mount + +test -d /etc/hoge +test ! -d /run/hoge +test -d /var/lib/hoge +test -d /var/cache/hoge +test -d /var/log/hoge + +systemctl clean tmp-hoge.mount --what=configuration + +test ! -d /etc/hoge +test ! -d /run/hoge +test -d /var/lib/hoge +test -d /var/cache/hoge +test -d /var/log/hoge + +systemctl clean tmp-hoge.mount + +test ! -d /etc/hoge +test ! -d /run/hoge +test -d /var/lib/hoge +test ! -d /var/cache/hoge +test -d /var/log/hoge + +systemctl clean tmp-hoge.mount --what=logs + +test ! -d /etc/hoge +test ! -d /run/hoge +test -d /var/lib/hoge +test ! -d /var/cache/hoge +test ! -d /var/log/hoge + +systemctl clean tmp-hoge.mount --what=all + +test ! -d /etc/hoge +test ! -d /run/hoge +test ! -d /var/lib/hoge +test ! -d /var/cache/hoge +test ! -d /var/log/hoge + +cat >/run/systemd/system/test-service.socket <<EOF +[Socket] +ListenSequentialPacket=/run/test-service.socket +RemoveOnStop=yes +ExecStartPre=true +ConfigurationDirectory=test-socket +RuntimeDirectory=test-socket +StateDirectory=test-socket +CacheDirectory=test-socket +LogsDirectory=test-socket +EOF + +systemctl daemon-reload + +test ! -e /etc/test-socket +test ! -e /run/test-socket +test ! -e /var/lib/test-socket +test ! -e /var/cache/test-socket +test ! -e /var/log/test-socket + +systemctl start test-service.socket + +test -d /etc/test-socket +test -d /run/test-socket +test -d /var/lib/test-socket +test -d /var/cache/test-socket +test -d /var/log/test-socket + +(! systemctl clean test-service.socket) + +systemctl stop test-service.socket + +test -d /etc/test-socket +test ! -d /run/test-socket +test -d /var/lib/test-socket +test -d /var/cache/test-socket +test -d /var/log/test-socket + +systemctl clean test-service.socket --what=configuration + +test ! -e /etc/test-socket +test ! -d /run/test-socket +test -d /var/lib/test-socket +test -d /var/cache/test-socket +test -d /var/log/test-socket + +systemctl clean test-service.socket + +test ! -e /etc/test-socket +test ! -e /run/test-socket +test -d /var/lib/test-socket +test ! -e /var/cache/test-socket +test -d /var/log/test-socket + +systemctl clean test-service.socket --what=logs + +test ! -e /etc/test-socket +test ! -e /run/test-socket +test -d /var/lib/test-socket +test ! -e /var/cache/test-socket +test ! -e /var/log/test-socket + +systemctl clean test-service.socket --what=all + +test ! -e /etc/test-socket +test ! -e /run/test-socket +test ! -e /var/lib/test-socket +test ! -e /var/cache/test-socket +test ! -e /var/log/test-socket diff --git a/test/units/testsuite-23.exec-command-ex.sh b/test/units/testsuite-23.exec-command-ex.sh new file mode 100755 index 0000000..f926e7d --- /dev/null +++ b/test/units/testsuite-23.exec-command-ex.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test ExecXYZEx= service unit dbus hookups + +systemd-analyze log-level debug + +declare -A property + +property[1_one]=ExecCondition +property[2_two]=ExecStartPre +property[3_three]=ExecStart +property[4_four]=ExecStartPost +property[5_five]=ExecReload +property[6_six]=ExecStop +property[7_seven]=ExecStopPost + +# These should all get upgraded to the corresponding Ex property as the non-Ex variant +# does not support the ":" prefix (no-env-expand). +for c in "${!property[@]}"; do + systemd-run --unit="$c" -r -p "Type=oneshot" -p "${property[$c]}=:/bin/echo \${$c}" /bin/true + systemctl show -p "${property[$c]}" "$c" | grep -F "path=/bin/echo ; argv[]=/bin/echo \${$c} ; ignore_errors=no" + systemctl show -p "${property[$c]}Ex" "$c" | grep -F "path=/bin/echo ; argv[]=/bin/echo \${$c} ; flags=no-env-expand" +done + +declare -A property_ex + +property_ex[1_one_ex]=ExecConditionEx +property_ex[2_two_ex]=ExecStartPreEx +property_ex[3_three_ex]=ExecStartEx +property_ex[4_four_ex]=ExecStartPostEx +property_ex[5_five_ex]=ExecReloadEx +property_ex[6_six_ex]=ExecStopEx +property_ex[7_seven_ex]=ExecStopPostEx + +for c in "${!property_ex[@]}"; do + systemd-run --unit="$c" -r -p "Type=oneshot" -p "${property_ex[$c]}=:/bin/echo \${$c}" /bin/true + systemctl show -p "${property_ex[$c]%??}" "$c" | grep -F "path=/bin/echo ; argv[]=/bin/echo \${$c} ; ignore_errors=no" + systemctl show -p "${property_ex[$c]}" "$c" | grep -F "path=/bin/echo ; argv[]=/bin/echo \${$c} ; flags=no-env-expand" +done + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.oneshot-restart.sh b/test/units/testsuite-23.oneshot-restart.sh new file mode 100755 index 0000000..433cd69 --- /dev/null +++ b/test/units/testsuite-23.oneshot-restart.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test oneshot unit restart on failure + +# wait this many secs for each test service to succeed in what is being tested +MAX_SECS=60 + +systemd-analyze log-level debug + +# test one: Restart=on-failure should restart the service +(! systemd-run --unit=oneshot-restart-one -p Type=oneshot -p Restart=on-failure /bin/bash -c "exit 1") + +for ((secs = 0; secs < MAX_SECS; secs++)); do + [[ "$(systemctl show oneshot-restart-one.service -P NRestarts)" -le 0 ]] || break + sleep 1 +done +if [[ "$(systemctl show oneshot-restart-one.service -P NRestarts)" -le 0 ]]; then + exit 1 +fi + +TMP_FILE="/tmp/test-41-oneshot-restart-test" + +: >$TMP_FILE + +# test two: make sure StartLimitBurst correctly limits the number of restarts +# and restarts execution of the unit from the first ExecStart= +(! systemd-run --unit=oneshot-restart-two \ + -p StartLimitIntervalSec=120 \ + -p StartLimitBurst=3 \ + -p Type=oneshot \ + -p Restart=on-failure \ + -p ExecStart="/bin/bash -c \"printf a >>$TMP_FILE\"" /bin/bash -c "exit 1") + +# wait for at least 3 restarts +for ((secs = 0; secs < MAX_SECS; secs++)); do + [[ $(cat $TMP_FILE) != "aaa" ]] || break + sleep 1 +done +if [[ $(cat $TMP_FILE) != "aaa" ]]; then + exit 1 +fi + +# wait for 5 more seconds to make sure there aren't excess restarts +sleep 5 +if [[ $(cat $TMP_FILE) != "aaa" ]]; then + exit 1 +fi + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.percentj-wantedby.sh b/test/units/testsuite-23.percentj-wantedby.sh new file mode 100755 index 0000000..e9ffaba --- /dev/null +++ b/test/units/testsuite-23.percentj-wantedby.sh @@ -0,0 +1,15 @@ +#!/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 +set -eux +set -o pipefail + +# Ensure %j Wants directives work +systemd-run --wait \ + --property="Type=oneshot" \ + --property="Wants=testsuite-23-specifier-j-wants.service" \ + --property="After=testsuite-23-specifier-j-wants.service" \ + true + +test -f /tmp/tetsuite-23-specifier-j-done diff --git a/test/units/testsuite-23.runtime-bind-paths.sh b/test/units/testsuite-23.runtime-bind-paths.sh new file mode 100755 index 0000000..65c2dbf --- /dev/null +++ b/test/units/testsuite-23.runtime-bind-paths.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# Test adding new BindPaths while unit is already running + +at_exit() { + set +e + + rm -f /run/testsuite-23-marker-{fixed,runtime} + rm -fr /run/inaccessible +} + +trap at_exit EXIT + +echo "MARKER_FIXED" >/run/testsuite-23-marker-fixed +mkdir /run/inaccessible + +systemctl start testsuite-23-namespaced.service + +# Ensure that inaccessible paths aren't bypassed by the runtime setup, +(! systemctl bind --mkdir testsuite-23-namespaced.service /run/testsuite-23-marker-fixed /run/inaccessible/testfile-marker-fixed) + +echo "MARKER_WRONG" >/run/testsuite-23-marker-wrong +echo "MARKER_RUNTIME" >/run/testsuite-23-marker-runtime + +# Mount twice to exercise mount-beneath (on kernel 6.5+, on older kernels it will just overmount) +systemctl bind --mkdir testsuite-23-namespaced.service /run/testsuite-23-marker-wrong /tmp/testfile-marker-runtime +test "$(systemctl show -P SubState testsuite-23-namespaced.service)" = "running" +systemctl bind --mkdir testsuite-23-namespaced.service /run/testsuite-23-marker-runtime /tmp/testfile-marker-runtime + +timeout 10 bash -xec 'while [[ "$(systemctl show -P SubState testsuite-23-namespaced.service)" == running ]]; do sleep .5; done' +systemctl is-active testsuite-23-namespaced.service + +# Now test that systemctl bind fails when attempted on a non-namespaced unit +systemctl start testsuite-23-non-namespaced.service + +(! systemctl bind --mkdir testsuite-49-non-namespaced.service /run/testsuite-23-marker-runtime /tmp/testfile-marker-runtime) + +timeout 10 bash -xec 'while [[ "$(systemctl show -P SubState testsuite-23-non-namespaced.service)" == running ]]; do sleep .5; done' +(! systemctl is-active testsuite-23-non-namespaced.service) diff --git a/test/units/testsuite-23.service b/test/units/testsuite-23.service new file mode 100644 index 0000000..26f5226 --- /dev/null +++ b/test/units/testsuite-23.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-23-TYPE-EXEC + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-23.sh b/test/units/testsuite-23.sh new file mode 100755 index 0000000..a929c8b --- /dev/null +++ b/test/units/testsuite-23.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +# Note: the signal shenanigans are necessary for the Upholds= tests +run_subtests_with_signals SIGUSR1 SIGUSR2 SIGRTMIN+1 + +touch /testok diff --git a/test/units/testsuite-23.start-stop-no-reload.sh b/test/units/testsuite-23.start-stop-no-reload.sh new file mode 100755 index 0000000..9c4f17d --- /dev/null +++ b/test/units/testsuite-23.start-stop-no-reload.sh @@ -0,0 +1,93 @@ +#!/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 +set -eux +set -o pipefail + +# Test start & stop operations without daemon-reload + +at_exit() { + set +e + + rm -f /run/systemd/system/testsuite-23-no-reload.{service,target} +} + +trap at_exit EXIT + +cat >/run/systemd/system/testsuite-23-no-reload.target <<EOF +[Unit] +Wants=testsuite-23-no-reload.service +EOF + +systemctl daemon-reload + +systemctl start testsuite-23-no-reload.target + +# The filesystem on the test image, despite being ext4, seems to have a mtime +# granularity of one second, which means the manager's unit cache won't be +# marked as dirty when writing the unit file, unless we wait at least a full +# second after the previous daemon-reload. +# May 07 23:12:20 H testsuite-48.sh[30]: + cat +# May 07 23:12:20 H testsuite-48.sh[30]: + ls -l --full-time /etc/systemd/system/testsuite-23-no-reload.service +# May 07 23:12:20 H testsuite-48.sh[52]: -rw-r--r-- 1 root root 50 2020-05-07 23:12:20.000000000 +0100 / +# May 07 23:12:20 H testsuite-48.sh[30]: + stat -f --format=%t /etc/systemd/system/testsuite-23-no-reload.servic +# May 07 23:12:20 H testsuite-48.sh[53]: ef53 +sleep 3.1 + +cat >/run/systemd/system/testsuite-23-no-reload.service <<EOF +[Service] +ExecStart=/bin/sleep infinity +EOF + +systemctl start testsuite-23-no-reload.service + +systemctl is-active testsuite-23-no-reload.service + +# Stop and remove, and try again to exercise https://github.com/systemd/systemd/issues/15992 +systemctl stop testsuite-23-no-reload.service +rm -f /run/systemd/system/testsuite-23-no-reload.service +systemctl daemon-reload + +sleep 3.1 + +cat >/run/systemd/system/testsuite-23-no-reload.service <<EOF +[Service] +ExecStart=/bin/sleep infinity +EOF + +# Start a non-existing unit first, so that the cache is reloaded for an unrelated +# reason. Starting the existing unit later should still work thanks to the check +# for the last load attempt vs cache timestamp. +systemctl start testsuite-23-no-reload-nonexistent.service || true + +systemctl start testsuite-23-no-reload.service + +systemctl is-active testsuite-23-no-reload.service + +# Stop and remove, and try again to exercise the transaction setup code path by +# having the target pull in the unloaded but available unit +systemctl stop testsuite-23-no-reload.service testsuite-23-no-reload.target +rm -f /run/systemd/system/testsuite-23-no-reload.service /run/systemd/system/testsuite-23-no-reload.target +systemctl daemon-reload + +sleep 3.1 + +cat >/run/systemd/system/testsuite-23-no-reload.target <<EOF +[Unit] +Conflicts=shutdown.target +Wants=testsuite-23-no-reload.service +EOF + +systemctl daemon-reload + +systemctl start testsuite-23-no-reload.target + +cat >/run/systemd/system/testsuite-23-no-reload.service <<EOF +[Service] +ExecStart=/bin/sleep infinity +EOF + +systemctl restart testsuite-23-no-reload.target + +systemctl is-active testsuite-23-no-reload.service diff --git a/test/units/testsuite-23.statedir.sh b/test/units/testsuite-23.statedir.sh new file mode 100755 index 0000000..b592314 --- /dev/null +++ b/test/units/testsuite-23.statedir.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +set -eux +set -o pipefail + +# Test unit configuration/state/cache/log/runtime data cleanup + +export HOME=/root +export XDG_RUNTIME_DIR=/run/user/0 + +systemctl start user@0.service + +( ! test -d "$HOME"/.local/state/foo) +( ! test -d "$HOME"/.config/foo) + +systemd-run --user -p StateDirectory=foo --wait /bin/true + +test -d "$HOME"/.local/state/foo +( ! test -L "$HOME"/.local/state/foo) +( ! test -d "$HOME"/.config/foo) + +systemd-run --user -p StateDirectory=foo -p ConfigurationDirectory=foo --wait /bin/true + +test -d "$HOME"/.local/state/foo +( ! test -L "$HOME"/.local/state/foo) +test -d "$HOME"/.config/foo + +rmdir "$HOME"/.local/state/foo "$HOME"/.config/foo + +systemd-run --user -p StateDirectory=foo -p ConfigurationDirectory=foo --wait /bin/true + +test -d "$HOME"/.local/state/foo +( ! test -L "$HOME"/.local/state/foo) +test -d "$HOME"/.config/foo + +rmdir "$HOME"/.local/state/foo "$HOME"/.config/foo + +# Now trigger an update scenario by creating a config dir first +systemd-run --user -p ConfigurationDirectory=foo --wait /bin/true + +( ! test -d "$HOME"/.local/state/foo) +test -d "$HOME"/.config/foo + +# This will look like an update and result in a symlink +systemd-run --user -p StateDirectory=foo -p ConfigurationDirectory=foo --wait /bin/true + +test -d "$HOME"/.local/state/foo +test -L "$HOME"/.local/state/foo +test -d "$HOME"/.config/foo + +test "$(readlink "$HOME"/.local/state/foo)" = ../../.config/foo + +# Check that this will work safely a second time +systemd-run --user -p StateDirectory=foo -p ConfigurationDirectory=foo --wait /bin/true + +rm "$HOME"/.local/state/foo +rmdir "$HOME"/.config/foo diff --git a/test/units/testsuite-23.success-failure.sh b/test/units/testsuite-23.success-failure.sh new file mode 100755 index 0000000..8fc9596 --- /dev/null +++ b/test/units/testsuite-23.success-failure.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test OnSuccess=/OnFailure= in combination + +systemd-analyze log-level debug + +# Start-up should fail, but the automatic restart should fix it +(! systemctl start success-failure-test ) + +# Wait until the first invocation finished & failed +while test ! -f /tmp/success-failure-test-ran ; do + sleep .5 +done + +# Wait until the second invocation finished & succeeded +while test ! -f /tmp/success-failure-test-ran2 ; do + sleep .5 +done + +# Verify it is indeed running +systemctl is-active -q success-failure-test + +# The above should have caused the failure service to start (asynchronously) +while test "$(systemctl is-active success-failure-test-failure)" != "active" ; do + sleep .5 +done + +# But the success service should not have started +test "$(systemctl is-active success-failure-test-success)" = "inactive" + +systemctl stop success-failure-test-failure + +# Do a clean kill of the service now +systemctl kill success-failure-test + +# This should result in the success service to start +while test "$(systemctl is-active success-failure-test-success)" != "active" ; do + sleep .5 +done + +# But the failure service should not have started again +test "$(systemctl is-active success-failure-test-failure)" = "inactive" + +systemctl stop success-failure-test success-failure-test-success + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.type-exec.sh b/test/units/testsuite-23.type-exec.sh new file mode 100755 index 0000000..87f32cc --- /dev/null +++ b/test/units/testsuite-23.type-exec.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test Type=exec + +systemd-analyze log-level debug + +# Create a binary for which execve() will fail +touch /tmp/brokenbinary +chmod +x /tmp/brokenbinary + +# These three commands should succeed. +systemd-run --unit=exec-one -p Type=simple /bin/sleep infinity +systemd-run --unit=exec-two -p Type=simple -p User=idontexist /bin/sleep infinity +systemd-run --unit=exec-three -p Type=simple /tmp/brokenbinary + +# And now, do the same with Type=exec, where the latter two should fail +systemd-run --unit=exec-four -p Type=exec /bin/sleep infinity +(! systemd-run --unit=exec-five -p Type=exec -p User=idontexist /bin/sleep infinity) +(! systemd-run --unit=exec-six -p Type=exec /tmp/brokenbinary) + +systemd-run --unit=exec-seven -p KillSignal=SIGTERM -p RestartKillSignal=SIGINT -p Type=exec /bin/sleep infinity +# Both TERM and SIGINT happen to have the same number on all architectures +test "$(systemctl show --value -p KillSignal exec-seven.service)" -eq 15 +test "$(systemctl show --value -p RestartKillSignal exec-seven.service)" -eq 2 + +systemctl restart exec-seven.service +systemctl stop exec-seven.service + +# For issue #20933 + +# Should work normally +busctl call \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager StartTransientUnit \ + "ssa(sv)a(sa(sv))" test-20933-ok.service replace 1 \ + ExecStart "a(sasb)" 1 \ + /usr/bin/sleep 2 /usr/bin/sleep 1 true \ + 0 + +# DBus call should fail but not crash systemd +(! busctl call \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager StartTransientUnit \ + "ssa(sv)a(sa(sv))" test-20933-bad.service replace 1 \ + ExecStart "a(sasb)" 1 \ + /usr/bin/sleep 0 true \ + 0) + +# Same but with the empty argv in the middle +(! busctl call \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager StartTransientUnit \ + "ssa(sv)a(sa(sv))" test-20933-bad-middle.service replace 1 \ + ExecStart "a(sasb)" 3 \ + /usr/bin/sleep 2 /usr/bin/sleep 1 true \ + /usr/bin/sleep 0 true \ + /usr/bin/sleep 2 /usr/bin/sleep 1 true \ + 0) + +systemd-analyze log-level info diff --git a/test/units/testsuite-23.utmp.sh b/test/units/testsuite-23.utmp.sh new file mode 100755 index 0000000..4f84315 --- /dev/null +++ b/test/units/testsuite-23.utmp.sh @@ -0,0 +1,22 @@ +#!/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 + +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +USER="test-23-utmp" + +cleanup() { + userdel "$USER" +} + +trap cleanup EXIT +useradd "$USER" + +assert_eq "$(systemd-run -qP -p UtmpIdentifier=test -p UtmpMode=user -p User=$USER whoami)" "$USER" +assert_eq "$(systemd-run -qP -p UtmpIdentifier=test -p UtmpMode=user whoami)" "$(whoami)" diff --git a/test/units/testsuite-23.whoami.sh b/test/units/testsuite-23.whoami.sh new file mode 100755 index 0000000..a0c73b8 --- /dev/null +++ b/test/units/testsuite-23.whoami.sh @@ -0,0 +1,15 @@ +#!/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 +set -eux +set -o pipefail + +test "$(systemctl whoami)" = testsuite-23.service +test "$(systemctl whoami $$)" = testsuite-23.service + +systemctl whoami 1 $$ 1 | cmp - /dev/fd/3 3<<'EOF' +init.scope +testsuite-23.service +init.scope +EOF diff --git a/test/units/testsuite-24.service b/test/units/testsuite-24.service new file mode 100644 index 0000000..e192d1c --- /dev/null +++ b/test/units/testsuite-24.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-24-CRYPTSETUP +After=multi-user.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-24.sh b/test/units/testsuite-24.sh new file mode 100755 index 0000000..c815f90 --- /dev/null +++ b/test/units/testsuite-24.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# TODO: +# - /proc/cmdline parsing +# - figure out token support (apart from TPM2, as that's covered by TEST-70-TPM2) +# - this might help https://www.qemu.org/docs/master/system/devices/ccid.html +# - expect + interactive auth? + +# We set up an encrypted /var partition which should get mounted automatically +# on boot +mountpoint /var + +systemctl --state=failed --no-legend --no-pager | tee /failed +if [[ -s /failed ]]; then + echo >&2 "Found units in failed state" + exit 1 +fi + +at_exit() { + set +e + + mountpoint -q /proc/cmdline && umount /proc/cmdline + rm -f /etc/crypttab + [[ -e /tmp/crypttab.bak ]] && cp -fv /tmp/crypttab.bak /etc/crypttab + [[ -n "${STORE_LOOP:-}" ]] && losetup -d "$STORE_LOOP" + [[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR" + + systemctl daemon-reload +} + +trap at_exit EXIT + +cryptsetup_start_and_check() { + local expect_fail=0 + local ec volume unit + + if [[ "${1:?}" == "-f" ]]; then + expect_fail=1 + shift + fi + + for volume in "$@"; do + unit="systemd-cryptsetup@$volume.service" + + # The unit existence check should always pass + [[ "$(systemctl show -P LoadState "$unit")" == loaded ]] + systemctl list-unit-files "$unit" + + systemctl start "$unit" && ec=0 || ec=$? + if [[ "$expect_fail" -ne 0 ]]; then + if [[ "$ec" -eq 0 ]]; then + echo >&2 "Unexpected pass when starting $unit" + return 1 + fi + + return 0 + fi + + if [[ "$ec" -ne 0 ]]; then + echo >&2 "Unexpected fail when starting $unit" + return 1 + fi + + systemctl status "$unit" + test -e "/dev/mapper/$volume" + systemctl stop "$unit" + test ! -e "/dev/mapper/$volume" + done + + return 0 +} + +# Note: some stuff (especially TPM-related) is already tested by TEST-70-TPM2, +# so focus more on other areas instead + +# Use a common workdir to make the cleanup easier +WORKDIR="$(mktemp -d)" + +# Prepare a couple of LUKS2-encrypted disk images +# +# 1) Image with an empty password +IMAGE_EMPTY="$WORKDIR/empty.img)" +IMAGE_EMPTY_KEYFILE="$WORKDIR/empty.keyfile" +IMAGE_EMPTY_KEYFILE_ERASE="$WORKDIR/empty-erase.keyfile" +IMAGE_EMPTY_KEYFILE_ERASE_FAIL="$WORKDIR/empty-erase-fail.keyfile)" +truncate -s 32M "$IMAGE_EMPTY" +echo -n passphrase >"$IMAGE_EMPTY_KEYFILE" +chmod 0600 "$IMAGE_EMPTY_KEYFILE" +cryptsetup luksFormat --batch-mode \ + --pbkdf pbkdf2 \ + --pbkdf-force-iterations 1000 \ + --use-urandom \ + "$IMAGE_EMPTY" "$IMAGE_EMPTY_KEYFILE" +PASSWORD=passphrase NEWPASSWORD="" systemd-cryptenroll --password "$IMAGE_EMPTY" +# Duplicate the key file to test keyfile-erase as well +cp -v "$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY_KEYFILE_ERASE" +# The key should get erased even on a failed attempt, so test that too +cp -v "$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY_KEYFILE_ERASE_FAIL" + +# 2) Image with a detached header and a key file offset + size +IMAGE_DETACHED="$WORKDIR/detached.img" +IMAGE_DETACHED_KEYFILE="$WORKDIR/detached.keyfile" +IMAGE_DETACHED_KEYFILE2="$WORKDIR/detached.keyfile2" +IMAGE_DETACHED_HEADER="$WORKDIR/detached.header" +truncate -s 32M "$IMAGE_DETACHED" +dd if=/dev/urandom of="$IMAGE_DETACHED_KEYFILE" count=64 bs=1 +dd if=/dev/urandom of="$IMAGE_DETACHED_KEYFILE2" count=32 bs=1 +chmod 0600 "$IMAGE_DETACHED_KEYFILE" "$IMAGE_DETACHED_KEYFILE2" +cryptsetup luksFormat --batch-mode \ + --pbkdf pbkdf2 \ + --pbkdf-force-iterations 1000 \ + --use-urandom \ + --header "$IMAGE_DETACHED_HEADER" \ + --keyfile-offset 32 \ + --keyfile-size 16 \ + "$IMAGE_DETACHED" "$IMAGE_DETACHED_KEYFILE" +# Also, add a second key file to key slot 8 +# Note: --key-slot= behaves as --new-key-slot= when used alone for backwards compatibility +cryptsetup luksAddKey --batch-mode \ + --header "$IMAGE_DETACHED_HEADER" \ + --key-file "$IMAGE_DETACHED_KEYFILE" \ + --keyfile-offset 32 \ + --keyfile-size 16 \ + --key-slot 8 \ + "$IMAGE_DETACHED" "$IMAGE_DETACHED_KEYFILE2" + +# Prepare a couple of dummy devices we'll store a copy of the detached header +# and one of the keys on to test if systemd-cryptsetup correctly mounts them +# when necessary +STORE_IMAGE="$WORKDIR/store.img" +truncate -s 64M "$STORE_IMAGE" +STORE_LOOP="$(losetup --show --find --partscan "$STORE_IMAGE")" +sfdisk "$STORE_LOOP" <<EOF +label: gpt +type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=header_store size=32M +type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=keyfile_store +EOF +udevadm settle --timeout=30 +mkdir -p /mnt +mkfs.ext4 -L header_store "/dev/disk/by-partlabel/header_store" +mount "/dev/disk/by-partlabel/header_store" /mnt +cp "$IMAGE_DETACHED_HEADER" /mnt/header +umount /mnt +mkfs.ext4 -L keyfile_store "/dev/disk/by-partlabel/keyfile_store" +mount "/dev/disk/by-partlabel/keyfile_store" /mnt +cp "$IMAGE_DETACHED_KEYFILE2" /mnt/keyfile +umount /mnt +udevadm settle --timeout=30 + +# Prepare our test crypttab +[[ -e /etc/crypttab ]] && cp -fv /etc/crypttab /tmp/crypttab.bak +cat >/etc/crypttab <<EOF +# headless should translate to headless=1 +empty_key $IMAGE_EMPTY $IMAGE_EMPTY_KEYFILE headless,x-systemd.device-timeout=1m +empty_key_erase $IMAGE_EMPTY $IMAGE_EMPTY_KEYFILE_ERASE headless=1,keyfile-erase=1 +empty_key_erase_fail $IMAGE_EMPTY $IMAGE_EMPTY_KEYFILE_ERASE_FAIL headless=1,keyfile-erase=1,keyfile-offset=4 +# Empty passphrase without try-empty-password(=yes) shouldn't work +empty_fail0 $IMAGE_EMPTY - headless=1 +empty_fail1 $IMAGE_EMPTY - headless=1,try-empty-password=0 +empty0 $IMAGE_EMPTY - headless=1,try-empty-password +empty1 $IMAGE_EMPTY - headless=1,try-empty-password=1 +# This one expects the key to be under /{etc,run}/cryptsetup-keys.d/empty_nokey.key +empty_nokey $IMAGE_EMPTY - headless=1 + +detached $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=32,keyfile-size=16 +detached_store0 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1,header=/header:LABEL=header_store,keyfile-offset=32,keyfile-size=16 +detached_store1 $IMAGE_DETACHED /keyfile:LABEL=keyfile_store headless=1,header=$IMAGE_DETACHED_HEADER +detached_store2 $IMAGE_DETACHED /keyfile:LABEL=keyfile_store headless=1,header=/header:LABEL=header_store +detached_fail0 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=32 +detached_fail1 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1,header=$IMAGE_DETACHED_HEADER +detached_fail2 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1 +detached_fail3 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=16,keyfile-size=16 +detached_fail4 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=32,keyfile-size=8 +detached_slot0 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE2 headless=1,header=$IMAGE_DETACHED_HEADER +detached_slot1 $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE2 headless=1,header=$IMAGE_DETACHED_HEADER,key-slot=8 +detached_slot_fail $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE2 headless=1,header=$IMAGE_DETACHED_HEADER,key-slot=0 +EOF + +# Temporarily drop luks.name=/luks.uuid= from the kernel command line, as it makes +# systemd-cryptsetup-generator ignore mounts from /etc/crypttab that are not also +# specified on the kernel command line +sed -r 's/luks.(name|uuid)=[^[:space:]+]//' /proc/cmdline >/tmp/cmdline.tmp +mount --bind /tmp/cmdline.tmp /proc/cmdline +# Run the systemd-cryptsetup-generator once explicitly, to collect coverage, +# as during daemon-reload we run generators in a sandbox +mkdir -p /tmp/systemd-cryptsetup-generator.out +/usr/lib/systemd/system-generators/systemd-cryptsetup-generator /tmp/systemd-cryptsetup-generator.out/ +systemctl daemon-reload +systemctl list-unit-files "systemd-cryptsetup@*" + +cryptsetup_start_and_check empty_key +test -e "$IMAGE_EMPTY_KEYFILE_ERASE" +cryptsetup_start_and_check empty_key_erase +test ! -e "$IMAGE_EMPTY_KEYFILE_ERASE" +test -e "$IMAGE_EMPTY_KEYFILE_ERASE_FAIL" +cryptsetup_start_and_check -f empty_key_erase_fail +test ! -e "$IMAGE_EMPTY_KEYFILE_ERASE_FAIL" +cryptsetup_start_and_check -f empty_fail{0..1} +cryptsetup_start_and_check empty{0..1} +# First, check if we correctly fail without any key +cryptsetup_start_and_check -f empty_nokey +# And now provide the key via /{etc,run}/cryptsetup-keys.d/ +mkdir -p /run/cryptsetup-keys.d +cp "$IMAGE_EMPTY_KEYFILE" /run/cryptsetup-keys.d/empty_nokey.key +cryptsetup_start_and_check empty_nokey + +cryptsetup_start_and_check detached +cryptsetup_start_and_check detached_store{0..2} +cryptsetup_start_and_check -f detached_fail{0..4} +cryptsetup_start_and_check detached_slot{0..1} +cryptsetup_start_and_check -f detached_slot_fail + +touch /testok diff --git a/test/units/testsuite-25.service b/test/units/testsuite-25.service new file mode 100644 index 0000000..503eabb --- /dev/null +++ b/test/units/testsuite-25.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-25-IMPORT + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-25.sh b/test/units/testsuite-25.sh new file mode 100755 index 0000000..b298c50 --- /dev/null +++ b/test/units/testsuite-25.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +export SYSTEMD_PAGER=cat + +dd if=/dev/urandom of=/var/tmp/testimage.raw bs=$((1024*1024+7)) count=5 + +# Test import +machinectl import-raw /var/tmp/testimage.raw +machinectl image-status testimage +test -f /var/lib/machines/testimage.raw +cmp /var/tmp/testimage.raw /var/lib/machines/testimage.raw + +# Test export +machinectl export-raw testimage /var/tmp/testimage2.raw +cmp /var/tmp/testimage.raw /var/tmp/testimage2.raw +rm /var/tmp/testimage2.raw + +# Test compressed export (gzip) +machinectl export-raw testimage /var/tmp/testimage2.raw.gz +gunzip /var/tmp/testimage2.raw.gz +cmp /var/tmp/testimage.raw /var/tmp/testimage2.raw +rm /var/tmp/testimage2.raw + +# Test clone +machinectl clone testimage testimage3 +test -f /var/lib/machines/testimage3.raw +machinectl image-status testimage3 +test -f /var/lib/machines/testimage.raw +machinectl image-status testimage +cmp /var/tmp/testimage.raw /var/lib/machines/testimage.raw +cmp /var/tmp/testimage.raw /var/lib/machines/testimage3.raw + +# Test removal +machinectl remove testimage +test ! -f /var/lib/machines/testimage.raw +(! machinectl image-status testimage) + +# Test export of clone +machinectl export-raw testimage3 /var/tmp/testimage3.raw +cmp /var/tmp/testimage.raw /var/tmp/testimage3.raw +rm /var/tmp/testimage3.raw + +# Test rename +machinectl rename testimage3 testimage4 +test -f /var/lib/machines/testimage4.raw +machinectl image-status testimage4 +test ! -f /var/lib/machines/testimage3.raw +(! machinectl image-status testimage3) +cmp /var/tmp/testimage.raw /var/lib/machines/testimage4.raw + +# Test export of rename +machinectl export-raw testimage4 /var/tmp/testimage4.raw +cmp /var/tmp/testimage.raw /var/tmp/testimage4.raw +rm /var/tmp/testimage4.raw + +# Test removal +machinectl remove testimage4 +test ! -f /var/lib/machines/testimage4.raw +(! machinectl image-status testimage4) + +# → And now, let's test directory trees ← # + +# Set up a directory we can import +mkdir /var/tmp/scratch +mv /var/tmp/testimage.raw /var/tmp/scratch/ +touch /var/tmp/scratch/anotherfile +mkdir /var/tmp/scratch/adirectory +echo "piep" >/var/tmp/scratch/adirectory/athirdfile + +# Test import-fs +machinectl import-fs /var/tmp/scratch/ +test -d /var/lib/machines/scratch +machinectl image-status scratch + +# Test export-tar +machinectl export-tar scratch /var/tmp/scratch.tar.gz +test -f /var/tmp/scratch.tar.gz +mkdir /var/tmp/extract +(cd /var/tmp/extract ; tar xzf /var/tmp/scratch.tar.gz) +diff -r /var/tmp/scratch/ /var/tmp/extract/ +rm -rf /var/tmp/extract + +# Test import-tar +machinectl import-tar /var/tmp/scratch.tar.gz scratch2 +test -d /var/lib/machines/scratch2 +machinectl image-status scratch2 +diff -r /var/tmp/scratch/ /var/lib/machines/scratch2 + +# Test removal +machinectl remove scratch +test ! -f /var/lib/machines/scratch +(! machinectl image-status scratch) + +# Test clone +machinectl clone scratch2 scratch3 +test -d /var/lib/machines/scratch2 +machinectl image-status scratch2 +test -d /var/lib/machines/scratch3 +machinectl image-status scratch3 +diff -r /var/tmp/scratch/ /var/lib/machines/scratch3 + +# Test removal +machinectl remove scratch2 +test ! -f /var/lib/machines/scratch2 +(! machinectl image-status scratch2) + +# Test rename +machinectl rename scratch3 scratch4 +test -d /var/lib/machines/scratch4 +machinectl image-status scratch4 +test ! -f /var/lib/machines/scratch3 +(! machinectl image-status scratch3) +diff -r /var/tmp/scratch/ /var/lib/machines/scratch4 + +# Test removal +machinectl remove scratch4 +test ! -f /var/lib/machines/scratch4 +(! machinectl image-status scratch4) + +# Test import-tar hyphen/stdin pipe behavior +# shellcheck disable=SC2002 +cat /var/tmp/scratch.tar.gz | machinectl import-tar - scratch5 +test -d /var/lib/machines/scratch5 +machinectl image-status scratch5 +diff -r /var/tmp/scratch/ /var/lib/machines/scratch5 + +# Test export-tar hyphen/stdout pipe behavior +mkdir -p /var/tmp/extract +machinectl export-tar scratch5 - | tar xvf - -C /var/tmp/extract/ +diff -r /var/tmp/scratch/ /var/tmp/extract/ +rm -rf /var/tmp/extract + +rm -rf /var/tmp/scratch + +# Test removal +machinectl remove scratch5 +test ! -f /var/lib/machines/scratch5 +(! machinectl image-status scratch5) + +touch /testok diff --git a/test/units/testsuite-26.service b/test/units/testsuite-26.service new file mode 100644 index 0000000..d8fdaff --- /dev/null +++ b/test/units/testsuite-26.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-26-SYSTEMCTL + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-26.sh b/test/units/testsuite-26.sh new file mode 100755 index 0000000..1e11c42 --- /dev/null +++ b/test/units/testsuite-26.sh @@ -0,0 +1,465 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +at_exit() { + if [[ -v UNIT_NAME && -e "/usr/lib/systemd/system/$UNIT_NAME" ]]; then + rm -fvr "/usr/lib/systemd/system/$UNIT_NAME" "/etc/systemd/system/$UNIT_NAME.d" "+4" + fi + + rm -f /etc/init.d/issue-24990 + return 0 +} + +trap at_exit EXIT + +# Create a simple unit file for testing +# Note: the service file is created under /usr on purpose to test +# the 'revert' verb as well +export UNIT_NAME="systemctl-test-$RANDOM.service" +cat >"/usr/lib/systemd/system/$UNIT_NAME" <<\EOF +[Unit] +Description=systemctl test + +[Service] +ExecStart=sleep infinity +ExecReload=true + +# For systemctl clean +CacheDirectory=%n +ConfigurationDirectory=%n +LogsDirectory=%n +RuntimeDirectory=%n +StateDirectory=%n + +[Install] +WantedBy=multi-user.target +EOF + +# Configure the preset setting for the unit file +mkdir /run/systemd/system-preset/ +echo "disable $UNIT_NAME" >/run/systemd/system-preset/99-systemd-test.preset + +EDITOR='true' script -ec 'systemctl edit "$UNIT_NAME"' /dev/null +[ ! -e "/etc/systemd/system/$UNIT_NAME.d/override.conf" ] + +printf '%s\n' '[Service]' 'ExecStart=' 'ExecStart=sleep 10d' >"+4" +EDITOR='mv' script -ec 'systemctl edit "$UNIT_NAME"' /dev/null +printf '%s\n' '[Service]' 'ExecStart=' 'ExecStart=sleep 10d' | cmp - "/etc/systemd/system/$UNIT_NAME.d/override.conf" + +printf '%b' '[Service]\n' 'ExecStart=\n' 'ExecStart=sleep 10d' >"+4" +EDITOR='mv' script -ec 'systemctl edit "$UNIT_NAME"' /dev/null +printf '%s\n' '[Service]' 'ExecStart=' 'ExecStart=sleep 10d' | cmp - "/etc/systemd/system/$UNIT_NAME.d/override.conf" + +# Double free when editing a template unit (#26483) +EDITOR='true' script -ec 'systemctl edit user@0' /dev/null + +# Argument help +systemctl --state help +systemctl --signal help +systemctl --type help + +# list-dependencies +systemctl list-dependencies systemd-journald +systemctl list-dependencies --after systemd-journald +systemctl list-dependencies --before systemd-journald +systemctl list-dependencies --after --reverse systemd-journald +systemctl list-dependencies --before --reverse systemd-journald +systemctl list-dependencies --plain systemd-journald + +# list-* verbs +systemctl list-units +systemctl list-units --recursive +systemctl list-units --type=socket +systemctl list-units --type=service,timer +# Compat: --type= allows load states for compatibility reasons +systemctl list-units --type=loaded +systemctl list-units --type=loaded,socket +systemctl list-units --legend=yes -a "systemd-*" +systemctl list-units --state=active +systemctl list-units --with-dependencies systemd-journald.service +systemctl list-units --with-dependencies --after systemd-journald.service +systemctl list-units --with-dependencies --before --reverse systemd-journald.service +systemctl list-sockets +systemctl list-sockets --legend=no -a "*journal*" +systemctl list-sockets --show-types +systemctl list-sockets --state=listening +systemctl list-timers -a -l +systemctl list-jobs +systemctl list-jobs --after +systemctl list-jobs --before +systemctl list-jobs --after --before +systemctl list-jobs "*" +systemctl list-dependencies sysinit.target --type=socket,mount +systemctl list-dependencies multi-user.target --state=active +systemctl list-dependencies sysinit.target --state=mounted --all +systemctl list-paths +systemctl list-paths --legend=no -a "systemd*" + +test_list_unit_files() { + systemctl list-unit-files "$@" + systemctl list-unit-files "$@" "*journal*" +} + +test_list_unit_files +test_list_unit_files --root=/ + +# is-* verbs +# Should return 4 for a missing unit file +assert_rc 4 systemctl --quiet is-active not-found.service +assert_rc 4 systemctl --quiet is-failed not-found.service +assert_rc 4 systemctl --quiet is-enabled not-found.service +# is-active: return 3 when the unit exists but inactive +assert_rc 3 systemctl --quiet is-active "$UNIT_NAME" +# is-enabled: return 1 when the unit exists but disabled +assert_rc 1 systemctl --quiet is-enabled "$UNIT_NAME" + +# Basic service management +systemctl start --show-transaction "$UNIT_NAME" +systemctl status -n 5 "$UNIT_NAME" +systemctl is-active "$UNIT_NAME" +systemctl reload -T "$UNIT_NAME" +systemctl restart -T "$UNIT_NAME" +systemctl try-restart --show-transaction "$UNIT_NAME" +systemctl try-reload-or-restart --show-transaction "$UNIT_NAME" +systemctl kill "$UNIT_NAME" +(! systemctl is-active "$UNIT_NAME") +systemctl restart "$UNIT_NAME" +systemctl is-active "$UNIT_NAME" +systemctl restart "$UNIT_NAME" +systemctl stop "$UNIT_NAME" +(! systemctl is-active "$UNIT_NAME") + +assert_eq "$(systemctl is-system-running)" "$(systemctl is-failed)" + +# enable/disable/preset +test_enable_disable_preset() { + (! systemctl is-enabled "$@" "$UNIT_NAME") + systemctl enable "$@" "$UNIT_NAME" + systemctl is-enabled "$@" -l "$UNIT_NAME" + # We created a preset file for this unit above with a "disable" policy + systemctl preset "$@" "$UNIT_NAME" + (! systemctl is-enabled "$@" "$UNIT_NAME") + systemctl reenable "$@" "$UNIT_NAME" + systemctl is-enabled "$@" "$UNIT_NAME" + systemctl preset "$@" --preset-mode=enable-only "$UNIT_NAME" + systemctl is-enabled "$@" "$UNIT_NAME" + systemctl preset "$@" --preset-mode=disable-only "$UNIT_NAME" + (! systemctl is-enabled "$@" "$UNIT_NAME") + systemctl enable "$@" --runtime "$UNIT_NAME" + [[ -e "/run/systemd/system/multi-user.target.wants/$UNIT_NAME" ]] + systemctl is-enabled "$@" "$UNIT_NAME" + systemctl disable "$@" "$UNIT_NAME" + # The unit should be still enabled, as we didn't use the --runtime switch + systemctl is-enabled "$@" "$UNIT_NAME" + systemctl disable "$@" --runtime "$UNIT_NAME" + (! systemctl is-enabled "$@" "$UNIT_NAME") +} + +test_enable_disable_preset +test_enable_disable_preset --root=/ + +# mask/unmask/revert +test_mask_unmask_revert() { + systemctl disable "$@" "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == disabled ]] + systemctl mask "$@" "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == masked ]] + systemctl unmask "$@" "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == disabled ]] + systemctl mask "$@" "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == masked ]] + systemctl revert "$@" "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == disabled ]] + systemctl mask "$@" --runtime "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == masked-runtime ]] + # This should be a no-op without the --runtime switch + systemctl unmask "$@" "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == masked-runtime ]] + systemctl unmask "$@" --runtime "$UNIT_NAME" + [[ "$(systemctl is-enabled "$@" "$UNIT_NAME")" == disabled ]] +} + +test_mask_unmask_revert +test_mask_unmask_revert --root=/ + +# add-wants/add-requires +(! systemctl show -P Wants "$UNIT_NAME" | grep "systemd-journald.service") +systemctl add-wants "$UNIT_NAME" "systemd-journald.service" +systemctl show -P Wants "$UNIT_NAME" | grep "systemd-journald.service" +(! systemctl show -P Requires "$UNIT_NAME" | grep "systemd-journald.service") +systemctl add-requires "$UNIT_NAME" "systemd-journald.service" +systemctl show -P Requires "$UNIT_NAME" | grep "systemd-journald.service" + +# set-property +systemctl set-property "$UNIT_NAME" IPAccounting=yes MemoryMax=1234567 +systemctl cat "$UNIT_NAME" +# These properties should be saved to a persistent storage +grep -r "IPAccounting=yes" "/etc/systemd/system.control/${UNIT_NAME}.d/" +grep -r "MemoryMax=1234567" "/etc/systemd/system.control/${UNIT_NAME}.d" +systemctl revert "$UNIT_NAME" +(! grep -r "IPAccounting=" "/etc/systemd/system.control/${UNIT_NAME}.d/") +(! grep -r "MemoryMax=" "/etc/systemd/system.control/${UNIT_NAME}.d/") +# Same stuff, but with --runtime, which should use /run +systemctl set-property --runtime "$UNIT_NAME" CPUAccounting=no CPUQuota=10% +systemctl cat "$UNIT_NAME" +grep -r "CPUAccounting=no" "/run/systemd/system.control/${UNIT_NAME}.d/" +grep -r "CPUQuota=10%" "/run/systemd/system.control/${UNIT_NAME}.d/" +systemctl revert "$UNIT_NAME" +(! grep -r "CPUAccounting=" "/run/systemd/system.control/${UNIT_NAME}.d/") +(! grep -r "CPUQuota=" "/run/systemd/system.control/${UNIT_NAME}.d/") + +# Failed-unit related tests +(! systemd-run --wait --unit "failed.service" /bin/false) +systemctl is-failed failed.service +systemctl --state=failed | grep failed.service +systemctl --failed | grep failed.service +systemctl reset-failed "fail*.service" +(! systemctl is-failed failed.service) + +# clean +systemctl restart "$UNIT_NAME" +systemctl stop "$UNIT_NAME" +# Check if the directories from *Directory= directives exist +# (except RuntimeDirectory= in /run, which is removed when the unit is stopped) +for path in /var/lib /var/cache /var/log /etc; do + [[ -e "$path/$UNIT_NAME" ]] +done +# Run the cleanup +for what in "" configuration state cache logs runtime all; do + systemctl clean ${what:+--what="$what"} "$UNIT_NAME" +done +# All respective directories should be removed +for path in /run /var/lib /var/cache /var/log /etc; do + [[ ! -e "$path/$UNIT_NAME" ]] +done + +# --timestamp +for value in pretty us µs utc us+utc µs+utc; do + systemctl show -P KernelTimestamp --timestamp="$value" +done + +# set-default/get-default +test_get_set_default() { + target="$(systemctl get-default "$@")" + systemctl set-default "$@" emergency.target + [[ "$(systemctl get-default "$@")" == emergency.target ]] + systemctl set-default "$@" "$target" + [[ "$(systemctl get-default "$@")" == "$target" ]] +} + +test_get_set_default +test_get_set_default --root=/ + +# show/status +systemctl show --property "" +# Pick a heavily sandboxed unit for the best effect on coverage +systemctl show systemd-logind.service +systemctl status +# Ignore the exit code in this case, as it might try to load non-existing units +systemctl status -a >/dev/null || : +systemctl status -a --state active,running,plugged >/dev/null +systemctl status "systemd-*.timer" +systemctl status "systemd-journald*.socket" +systemctl status "sys-devices-*-ttyS0.device" +systemctl status -- -.mount +systemctl status 1 + +# --marked +systemctl restart "$UNIT_NAME" +systemctl set-property "$UNIT_NAME" Markers=needs-restart +systemctl show -P Markers "$UNIT_NAME" | grep needs-restart +systemctl reload-or-restart --marked +(! systemctl show -P Markers "$UNIT_NAME" | grep needs-restart) + +# --dry-run with destructive verbs +# kexec is skipped intentionally, as it requires a bit more involved setup +VERBS=( + default + emergency + exit + halt + hibernate + hybrid-sleep + poweroff + reboot + rescue + suspend + suspend-then-hibernate +) + +for verb in "${VERBS[@]}"; do + systemctl --dry-run "$verb" + + if [[ "$verb" =~ (halt|poweroff|reboot) ]]; then + systemctl --dry-run --message "Hello world" "$verb" + systemctl --dry-run --no-wall "$verb" + systemctl --dry-run -f "$verb" + systemctl --dry-run -ff "$verb" + fi +done + +# Aux verbs & assorted checks +systemctl is-active "*-journald.service" +systemctl cat "*journal*" +systemctl cat "$UNIT_NAME" +systemctl help "$UNIT_NAME" +systemctl service-watchdogs +systemctl service-watchdogs "$(systemctl service-watchdogs)" + +# show/set-environment +# Make sure PATH is set +systemctl show-environment | grep -q '^PATH=' +# Let's add an entry and override a built-in one +systemctl set-environment PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/testaddition FOO=BAR +# Check that both are set +systemctl show-environment | grep -q '^PATH=.*testaddition$' +systemctl show-environment | grep -q '^FOO=BAR$' +systemctl daemon-reload +# Check again after the reload +systemctl show-environment | grep -q '^PATH=.*testaddition$' +systemctl show-environment | grep -q '^FOO=BAR$' +# Check that JSON output is supported +systemctl show-environment --output=json | grep -q '^{.*"FOO":"BAR".*}$' +# Drop both +systemctl unset-environment FOO PATH +# Check that one is gone and the other reverted to the built-in +systemctl show-environment | grep '^FOO=$' && exit 1 +systemctl show-environment | grep '^PATH=.*testaddition$' && exit 1 +systemctl show-environment | grep -q '^PATH=' +# Check import-environment +export IMPORT_THIS=hello +export IMPORT_THIS_TOO=world +systemctl import-environment IMPORT_THIS IMPORT_THIS_TOO +systemctl show-environment | grep "^IMPORT_THIS=$IMPORT_THIS" +systemctl show-environment | grep "^IMPORT_THIS_TOO=$IMPORT_THIS_TOO" +systemctl unset-environment IMPORT_THIS IMPORT_THIS_TOO +(! systemctl show-environment | grep "^IMPORT_THIS=") +(! systemctl show-environment | grep "^IMPORT_THIS_TOO=") + +# test for sysv-generator (issue #24990) +if [[ -x /usr/lib/systemd/system-generators/systemd-sysv-generator ]]; then + # This is configurable via -Dsysvinit-path=, but we can't get the value + # at runtime, so let's just support the two most common paths for now. + [[ -d /etc/rc.d/init.d ]] && SYSVINIT_PATH="/etc/rc.d/init.d" || SYSVINIT_PATH="/etc/init.d" + + # invalid dependency + cat >"${SYSVINIT_PATH:?}/issue-24990" <<\EOF +#!/bin/bash + +### BEGIN INIT INFO +# Provides:test1 test2 +# Required-Start:test1 $remote_fs $network +# Required-Stop:test1 $remote_fs $network +# Description:Test +# Short-Description: Test +### END INIT INFO + +case "$1" in + start) + echo "Starting issue-24990.service" + sleep 1000 & + ;; + stop) + echo "Stopping issue-24990.service" + sleep 10 & + ;; + *) + echo "Usage: service test {start|stop|restart|status}" + ;; +esac +EOF + + chmod +x "$SYSVINIT_PATH/issue-24990" + systemctl daemon-reload + [[ -L /run/systemd/generator.late/test1.service ]] + [[ -L /run/systemd/generator.late/test2.service ]] + assert_eq "$(readlink -f /run/systemd/generator.late/test1.service)" "/run/systemd/generator.late/issue-24990.service" + assert_eq "$(readlink -f /run/systemd/generator.late/test2.service)" "/run/systemd/generator.late/issue-24990.service" + output=$(systemctl cat issue-24990) + assert_in "SourcePath=$SYSVINIT_PATH/issue-24990" "$output" + assert_in "Description=LSB: Test" "$output" + assert_in "After=test1.service" "$output" + assert_in "After=remote-fs.target" "$output" + assert_in "After=network-online.target" "$output" + assert_in "Wants=network-online.target" "$output" + assert_in "ExecStart=$SYSVINIT_PATH/issue-24990 start" "$output" + assert_in "ExecStop=$SYSVINIT_PATH/issue-24990 stop" "$output" + systemctl status issue-24990 || : + systemctl show issue-24990 + assert_not_in "issue-24990.service" "$(systemctl show --property=After --value)" + assert_not_in "issue-24990.service" "$(systemctl show --property=Before --value)" + + if ! systemctl is-active network-online.target; then + systemctl start network-online.target + fi + + systemctl restart issue-24990 + systemctl stop issue-24990 + + # valid dependency + cat >"$SYSVINIT_PATH/issue-24990" <<\EOF +#!/bin/bash + +### BEGIN INIT INFO +# Provides:test1 test2 +# Required-Start:$remote_fs +# Required-Stop:$remote_fs +# Description:Test +# Short-Description: Test +### END INIT INFO + +case "$1" in + start) + echo "Starting issue-24990.service" + sleep 1000 & + ;; + stop) + echo "Stopping issue-24990.service" + sleep 10 & + ;; + *) + echo "Usage: service test {start|stop|restart|status}" + ;; +esac +EOF + + chmod +x "$SYSVINIT_PATH/issue-24990" + systemctl daemon-reload + [[ -L /run/systemd/generator.late/test1.service ]] + [[ -L /run/systemd/generator.late/test2.service ]] + assert_eq "$(readlink -f /run/systemd/generator.late/test1.service)" "/run/systemd/generator.late/issue-24990.service" + assert_eq "$(readlink -f /run/systemd/generator.late/test2.service)" "/run/systemd/generator.late/issue-24990.service" + output=$(systemctl cat issue-24990) + assert_in "SourcePath=$SYSVINIT_PATH/issue-24990" "$output" + assert_in "Description=LSB: Test" "$output" + assert_in "After=remote-fs.target" "$output" + assert_in "ExecStart=$SYSVINIT_PATH/issue-24990 start" "$output" + assert_in "ExecStop=$SYSVINIT_PATH/issue-24990 stop" "$output" + systemctl status issue-24990 || : + systemctl show issue-24990 + assert_not_in "issue-24990.service" "$(systemctl show --property=After --value)" + assert_not_in "issue-24990.service" "$(systemctl show --property=Before --value)" + + systemctl restart issue-24990 + systemctl stop issue-24990 +fi + +# %J in WantedBy= causes ABRT (#26467) +cat >/run/systemd/system/test-WantedBy.service <<EOF +[Service] +ExecStart=true + +[Install] +WantedBy=user-%i@%J.service +EOF +systemctl daemon-reload +systemctl enable --now test-WantedBy.service || : +systemctl daemon-reload + +touch /testok diff --git a/test/units/testsuite-29.service b/test/units/testsuite-29.service new file mode 100644 index 0000000..035c6bf --- /dev/null +++ b/test/units/testsuite-29.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-29-PORTABLE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-29.sh b/test/units/testsuite-29.sh new file mode 100755 index 0000000..5368273 --- /dev/null +++ b/test/units/testsuite-29.sh @@ -0,0 +1,280 @@ +#!/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 +set -eux +set -o pipefail + +# Set longer timeout for slower machines, e.g. non-KVM vm. +mkdir -p /run/systemd/system.conf.d +cat >/run/systemd/system.conf.d/10-timeout.conf <<EOF +[Manager] +DefaultEnvironment=SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30 +ManagerEnvironment=SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30 +EOF + +systemctl daemon-reexec + +export SYSTEMD_DISSECT_VERITY_TIMEOUT_SEC=30 + +udevadm control --log-level debug + +ARGS=() +STATE_DIRECTORY=/var/lib/private/ +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) + # With the trusted profile DynamicUser is disabled, so the storage is not in private/ + STATE_DIRECTORY=/var/lib/ +fi + +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' +systemd-dissect --no-pager /usr/share/app0.raw | grep -q '✓ sysext for portable service' +systemd-dissect --no-pager /usr/share/app1.raw | grep -q '✓ sysext for portable service' +systemd-dissect --no-pager /usr/share/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 + +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 /usr/share/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 /usr/share/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 /usr/share/app0.raw /usr/share/minimal_1.raw app0 + +portablectl "${ARGS[@]}" attach --now --runtime --extension /usr/share/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 /usr/share/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 /usr/share/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 /usr/share/app1.raw /usr/share/minimal_1.raw app1 +portablectl "${ARGS[@]}" attach --force --no-reload --runtime --extension /usr/share/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 /usr/share/app1.raw /usr/share/minimal_0.raw app1 + +# 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 /usr/share/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 /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0 + +systemctl is-active app0.service +status="$(portablectl is-attached --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw)" +[[ "${status}" == "running-runtime" ]] + +portablectl inspect --force --cat --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /usr/share/conf0.raw" + +portablectl detach --now --runtime --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0 + +# 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 /usr/share/app0.raw /tmp/app0 +mount /usr/share/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 + +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 --now --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 + +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 +touch /tmp/emptyroot/usr/lib/os-release +touch /tmp/emptyext/usr/lib/extension-release.d/extension-release.emptyext + +# Remote peer disconnected -> portabled crashed +res="$(! portablectl attach --extension /tmp/emptyext /tmp/emptyroot 2> >(grep "Remote peer disconnected"))" +test -z "${res}" + +touch /testok diff --git a/test/units/testsuite-30.service b/test/units/testsuite-30.service new file mode 100644 index 0000000..253f7b5 --- /dev/null +++ b/test/units/testsuite-30.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-30-ONCLOCKCHANGE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-30.sh b/test/units/testsuite-30.sh new file mode 100755 index 0000000..104c87b --- /dev/null +++ b/test/units/testsuite-30.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-analyze log-level debug + +systemctl disable --now systemd-timesyncd.service + +timedatectl set-timezone Europe/Berlin +timedatectl set-time 1980-10-15 + +systemd-run --on-timezone-change touch /tmp/timezone-changed +systemd-run --on-clock-change touch /tmp/clock-changed + +test ! -f /tmp/timezone-changed +test ! -f /tmp/clock-changed + +timedatectl set-timezone Europe/Kiev + +while test ! -f /tmp/timezone-changed ; do sleep .5 ; done + +timedatectl set-time 2018-1-1 + +while test ! -f /tmp/clock-changed ; do sleep .5 ; done + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-31.service b/test/units/testsuite-31.service new file mode 100644 index 0000000..f0e78a9 --- /dev/null +++ b/test/units/testsuite-31.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-31-DEVICE-ENUMERATION + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-31.sh b/test/units/testsuite-31.sh new file mode 100755 index 0000000..03aba36 --- /dev/null +++ b/test/units/testsuite-31.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if journalctl -b -t systemd --grep '\.device: Changed plugged -> dead'; then + exit 1 +fi + +touch /testok diff --git a/test/units/testsuite-32.service b/test/units/testsuite-32.service new file mode 100644 index 0000000..50f5823 --- /dev/null +++ b/test/units/testsuite-32.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-32-OOMPOLICY + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot +MemoryAccounting=yes diff --git a/test/units/testsuite-32.sh b/test/units/testsuite-32.sh new file mode 100755 index 0000000..83b548a --- /dev/null +++ b/test/units/testsuite-32.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Let's run this test only if the "memory.oom.group" cgroupfs attribute +# exists. This test is a bit too strict, since the "memory.events"/"oom_kill" +# logic has been around since a longer time than "memory.oom.group", but it's +# an easier thing to test for, and also: let's not get confused by older +# kernels where the concept was still new. + +if test -f /sys/fs/cgroup/system.slice/testsuite-32.service/memory.oom.group; then + systemd-analyze log-level debug + + # Run a service that is guaranteed to be the first candidate for OOM killing + systemd-run --unit=oomtest.service \ + -p Type=exec -p OOMScoreAdjust=1000 -p OOMPolicy=stop -p MemoryAccounting=yes \ + sleep infinity + + # Trigger an OOM killer run + echo 1 >/proc/sys/kernel/sysrq + echo f >/proc/sysrq-trigger + + while : ; do + STATE="$(systemctl show -P ActiveState oomtest.service)" + [ "$STATE" = "failed" ] && break + sleep .5 + done + + RESULT="$(systemctl show -P Result oomtest.service)" + test "$RESULT" = "oom-kill" + + systemd-analyze log-level info +fi + +touch /testok diff --git a/test/units/testsuite-34.service b/test/units/testsuite-34.service new file mode 100644 index 0000000..6917afe --- /dev/null +++ b/test/units/testsuite-34.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-34-DYNAMICUSERMIGRATE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-34.sh b/test/units/testsuite-34.sh new file mode 100755 index 0000000..d15b675 --- /dev/null +++ b/test/units/testsuite-34.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-analyze log-level debug + +test_directory() { + local directory="$1" + local path="$2" + + # cleanup for previous invocation + for i in xxx xxx2 yyy zzz x:yz x:yz2; do + rm -rf "${path:?}/${i}" "${path:?}/private/${i}" + done + + # Set everything up without DynamicUser=1 + + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz touch "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz test -f "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz -p TemporaryFileSystem="${path}" test -f "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:yyy test -f "${path}"/yyy/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}=zzz:xxx zzz:xxx2" -p TemporaryFileSystem="${path}" bash -c "test -f ${path}/xxx/test && test -f ${path}/xxx2/test" + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}":ro test -f "${path}"/xxx/test + (! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz test -f "${path}"/zzz/test-missing) + + test -d "${path}"/zzz + test ! -L "${path}"/zzz + test ! -e "${path}"/private/zzz + + test ! -e "${path}"/xxx + test ! -e "${path}"/private/xxx + test ! -e "${path}"/xxx2 + test ! -e "${path}"/private/xxx2 + test -L "${path}"/yyy + test ! -e "${path}"/private/yyy + + test -f "${path}"/zzz/test + test ! -e "${path}"/zzz/test-missing + + # Convert to DynamicUser=1 + + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz test -f "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz -p TemporaryFileSystem="${path}" test -f "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz:yyy test -f "${path}"/yyy/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}=zzz:xxx zzz:xxx2" \ + -p TemporaryFileSystem="${path}" -p EnvironmentFile=-/usr/lib/systemd/systemd-asan-env bash -c "test -f ${path}/xxx/test && test -f ${path}/xxx2/test" + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}":ro test -f "${path}"/xxx/test + (! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz test -f "${path}"/zzz/test-missing) + + test -L "${path}"/zzz + test -d "${path}"/private/zzz + + test ! -e "${path}"/xxx + test ! -e "${path}"/private/xxx + test ! -e "${path}"/xxx2 + test ! -e "${path}"/private/xxx2 + test -L "${path}"/yyy # previous symlink is not removed + test ! -e "${path}"/private/yyy + + test -f "${path}"/zzz/test + test ! -e "${path}"/zzz/test-missing + + # Convert back + + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz test -f "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz -p TemporaryFileSystem="${path}" test -f "${path}"/zzz/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:yyy test -f "${path}"/yyy/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}" test -f "${path}"/xxx/test + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}=zzz:xxx zzz:xxx2" -p TemporaryFileSystem="${path}" bash -c "test -f ${path}/xxx/test && test -f ${path}/xxx2/test" + systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}":ro test -f "${path}"/xxx/test + (! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz test -f "${path}"/zzz/test-missing) + + test -d "${path}"/zzz + test ! -L "${path}"/zzz + test ! -e "${path}"/private/zzz + + test ! -e "${path}"/xxx + test ! -e "${path}"/private/xxx + test ! -e "${path}"/xxx2 + test ! -e "${path}"/private/xxx2 + test -L "${path}"/yyy + test ! -e "${path}"/private/yyy + + test -f "${path}"/zzz/test + test ! -e "${path}"/zzz/test-missing + + # Exercise the unit parsing paths too + cat >/run/systemd/system/testservice-34.service <<EOF +[Service] +Type=oneshot +TemporaryFileSystem=${path} +RuntimeDirectoryPreserve=yes +${directory}=zzz:x\:yz zzz:x\:yz2 +ExecStart=test -f ${path}/x:yz2/test +ExecStart=test -f ${path}/x:yz/test +ExecStart=test -f ${path}/zzz/test +EOF + systemctl daemon-reload + systemctl start --wait testservice-34.service + + test -d "${path}"/zzz + test ! -L "${path}"/zzz + test ! -e "${path}"/private/zzz + + test ! -L "${path}"/x:yz + test ! -L "${path}"/x:yz2 +} + +test_check_writable() { + # cleanup for previous invocation + for i in aaa quux waldo xxx; do + rm -rf "/var/lib/$i" "/var/lib/private/$i" + done + + cat >/run/systemd/system/testservice-34-check-writable.service <<\EOF +[Unit] +Description=Check writable directories when DynamicUser= with StateDirectory= + +[Service] +# Relevant only for sanitizer runs +EnvironmentFile=-/usr/lib/systemd/systemd-asan-env + +Type=oneshot +DynamicUser=yes +StateDirectory=waldo quux/pief aaa/bbb aaa aaa/ccc xxx/yyy:aaa/111 xxx:aaa/222 xxx/zzz:aaa/333 + +# Make sure that the state directories are really the only writable directory besides the obvious candidates +ExecStart=bash -c ' \ + set -eux; \ + set -o pipefail; \ + declare -a writable_dirs; \ + readarray -t writable_dirs < <(find / \( -path /var/tmp -o -path /tmp -o -path /proc -o -path /dev/mqueue -o -path /dev/shm -o \ + -path /sys/fs/bpf -o -path /dev/.lxc -o -path /sys/devices/system/cpu \) \ + -prune -o -type d -writable -print 2>/dev/null | sort -u); \ + [[ "$${#writable_dirs[@]}" == "8" ]]; \ + [[ "$${writable_dirs[0]}" == "/var/lib/private/aaa" ]]; \ + [[ "$${writable_dirs[1]}" == "/var/lib/private/aaa/bbb" ]]; \ + [[ "$${writable_dirs[2]}" == "/var/lib/private/aaa/ccc" ]]; \ + [[ "$${writable_dirs[3]}" == "/var/lib/private/quux/pief" ]]; \ + [[ "$${writable_dirs[4]}" == "/var/lib/private/waldo" ]]; \ + [[ "$${writable_dirs[5]}" == "/var/lib/private/xxx" ]]; \ + [[ "$${writable_dirs[6]}" == "/var/lib/private/xxx/yyy" ]]; \ + [[ "$${writable_dirs[7]}" == "/var/lib/private/xxx/zzz" ]]; \ +' +EOF + systemctl daemon-reload + systemctl start testservice-34-check-writable.service +} + +test_directory "StateDirectory" "/var/lib" +test_directory "RuntimeDirectory" "/run" +test_directory "CacheDirectory" "/var/cache" +test_directory "LogsDirectory" "/var/log" + +test_check_writable + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-35.service b/test/units/testsuite-35.service new file mode 100644 index 0000000..0599f61 --- /dev/null +++ b/test/units/testsuite-35.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-35-LOGIN + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-35.sh b/test/units/testsuite-35.sh new file mode 100755 index 0000000..36e26da --- /dev/null +++ b/test/units/testsuite-35.sh @@ -0,0 +1,660 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +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 + +cleanup_test_user() ( + set +ex + + pkill -u "$(id -u logind-test-user)" + sleep 1 + pkill -KILL -u "$(id -u logind-test-user)" + userdel -r logind-test-user + + return 0 +) + +setup_test_user() { + mkdir -p /var/spool/cron /var/spool/mail + useradd -m -s /bin/bash logind-test-user + trap cleanup_test_user EXIT +} + +test_enable_debug() { + mkdir -p /run/systemd/system/systemd-logind.service.d + cat >/run/systemd/system/systemd-logind.service.d/debug.conf <<EOF +[Service] +Environment=SYSTEMD_LOG_LEVEL=debug +EOF + systemctl daemon-reload + systemctl stop systemd-logind.service +} + +testcase_properties() { + mkdir -p /run/systemd/logind.conf.d + + cat >/run/systemd/logind.conf.d/kill-user-processes.conf <<EOF +[Login] +KillUserProcesses=no +EOF + + systemctl restart systemd-logind.service + assert_eq "$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager KillUserProcesses)" "b false" + + cat >/run/systemd/logind.conf.d/kill-user-processes.conf <<EOF +[Login] +KillUserProcesses=yes +EOF + + systemctl restart systemd-logind.service + assert_eq "$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager KillUserProcesses)" "b true" + + rm -rf /run/systemd/logind.conf.d +} + +testcase_started() { + local pid + + systemctl restart systemd-logind.service + + # should start at boot, not with D-BUS activation + pid=$(systemctl show systemd-logind.service -p ExecMainPID --value) + + # loginctl should succeed + loginctl + + # logind should still be running + assert_eq "$(systemctl show systemd-logind.service -p ExecMainPID --value)" "$pid" +} + +wait_suspend() { + timeout "${1?}" bash -c "while [[ ! -e /run/suspend.flag ]]; do sleep 1; done" + rm /run/suspend.flag +} + +teardown_suspend() ( + set +eux + + pkill evemu-device + + rm -rf /run/systemd/system/systemd-suspend.service.d + systemctl daemon-reload + + rm -f /run/udev/rules.d/70-logindtest-lid.rules + udevadm control --reload + + return 0 +) + +testcase_suspend_on_lid() { + local pid input_name lid_dev + + if systemd-detect-virt --quiet --container; then + echo "Skipping suspend test in container" + return + fi + if ! grep -s -q mem /sys/power/state; then + echo "suspend not supported on this testbed, skipping" + return + fi + if ! command -v evemu-device >/dev/null; then + echo "command evemu-device not found, skipping" + return + fi + if ! command -v evemu-event >/dev/null; then + echo "command evemu-event not found, skipping" + return + fi + + trap teardown_suspend RETURN + + # save pid + pid=$(systemctl show systemd-logind.service -p ExecMainPID --value) + + # create fake suspend + mkdir -p /run/systemd/system/systemd-suspend.service.d + cat >/run/systemd/system/systemd-suspend.service.d/override.conf <<EOF +[Service] +ExecStart= +ExecStart=touch /run/suspend.flag +EOF + systemctl daemon-reload + + # create fake lid switch + mkdir -p /run/udev/rules.d + cat >/run/udev/rules.d/70-logindtest-lid.rules <<EOF +SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="Fake Lid Switch", TAG+="power-switch" +EOF + udevadm control --reload + + cat >/run/lidswitch.evemu <<EOF +# EVEMU 1.2 +# Input device name: "Lid Switch" +# Input device ID: bus 0x19 vendor 0000 product 0x05 version 0000 +# Supported events: +# Event type 0 (EV_SYN) +# Event code 0 (SYN_REPORT) +# Event code 5 (FF_STATUS_MAX) +# Event type 5 (EV_SW) +# Event code 0 (SW_LID) +# Properties: +N: Fake Lid Switch +I: 0019 0000 0005 0000 +P: 00 00 00 00 00 00 00 00 +B: 00 21 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 02 00 00 00 00 00 00 00 00 +B: 03 00 00 00 00 00 00 00 00 +B: 04 00 00 00 00 00 00 00 00 +B: 05 01 00 00 00 00 00 00 00 +B: 11 00 00 00 00 00 00 00 00 +B: 12 00 00 00 00 00 00 00 00 +B: 15 00 00 00 00 00 00 00 00 +B: 15 00 00 00 00 00 00 00 00 +EOF + + evemu-device /run/lidswitch.evemu & + + timeout 20 bash -c 'until grep "^Fake Lid Switch" /sys/class/input/*/device/name; do sleep .5; done' + input_name=$(grep -l '^Fake Lid Switch' /sys/class/input/*/device/name || :) + if [[ -z "$input_name" ]]; then + echo "cannot find fake lid switch." >&2 + exit 1 + fi + input_name=${input_name%/device/name} + lid_dev=/dev/${input_name#/sys/class/} + udevadm info --wait-for-initialization=10s "$lid_dev" + udevadm settle + + # close lid + evemu-event "$lid_dev" --sync --type 5 --code 0 --value 1 + # need to wait for 30s suspend inhibition after boot + wait_suspend 31 + # open lid again + evemu-event "$lid_dev" --sync --type 5 --code 0 --value 0 + + # waiting for 30s inhibition time between suspends + sleep 30 + + # now closing lid should cause instant suspend + evemu-event "$lid_dev" --sync --type 5 --code 0 --value 1 + wait_suspend 2 + evemu-event "$lid_dev" --sync --type 5 --code 0 --value 0 + + assert_eq "$(systemctl show systemd-logind.service -p ExecMainPID --value)" "$pid" +} + +testcase_shutdown() { + local pid + + # save pid + pid=$(systemctl show systemd-logind.service -p ExecMainPID --value) + + # scheduled shutdown with wall message + shutdown 2>&1 + sleep 5 + shutdown -c || : + # logind should still be running + assert_eq "$(systemctl show systemd-logind.service -p ExecMainPID --value)" "$pid" + + # scheduled shutdown without wall message + shutdown --no-wall 2>&1 + sleep 5 + shutdown -c --no-wall || true + assert_eq "$(systemctl show systemd-logind.service -p ExecMainPID --value)" "$pid" +} + +cleanup_session() ( + set +ex + + local uid s + + uid=$(id -u logind-test-user) + + loginctl disable-linger logind-test-user + + systemctl stop getty@tty2.service + + for s in $(loginctl --no-legend list-sessions | awk '$3 == "logind-test-user" { print $1 }'); do + echo "INFO: stopping session $s" + loginctl terminate-session "$s" + done + + loginctl terminate-user logind-test-user + + if ! timeout 30 bash -c "while loginctl --no-legend | grep -q logind-test-user; do sleep 1; done"; then + echo "WARNING: session for logind-test-user still active, ignoring." + fi + + pkill -u "$uid" + sleep 1 + pkill -KILL -u "$uid" + + if ! timeout 30 bash -c "while systemctl is-active --quiet user@${uid}.service; do sleep 1; done"; then + echo "WARNING: user@${uid}.service is still active, ignoring." + fi + + if ! timeout 30 bash -c "while systemctl is-active --quiet user-runtime-dir@${uid}.service; do sleep 1; done"; then + echo "WARNING: user-runtime-dir@${uid}.service is still active, ignoring." + fi + + if ! timeout 30 bash -c "while systemctl is-active --quiet user-${uid}.slice; do sleep 1; done"; then + echo "WARNING: user-${uid}.slice is still active, ignoring." + fi + + rm -rf /run/systemd/system/getty@tty2.service.d + systemctl daemon-reload + + return 0 +) + +teardown_session() ( + set +ex + + cleanup_session + + rm -f /run/udev/rules.d/70-logindtest-scsi_debug-user.rules + udevadm control --reload + rmmod scsi_debug + + return 0 +) + +check_session() ( + set +ex + + local seat session leader_pid + + if [[ $(loginctl --no-legend | grep -c "logind-test-user") != 1 ]]; then + echo "no session or multiple sessions for logind-test-user." >&2 + return 1 + fi + + seat=$(loginctl --no-legend | grep 'logind-test-user *seat' | awk '{ print $4 }') + if [[ -z "$seat" ]]; then + echo "no seat found for user logind-test-user" >&2 + return 1 + fi + + session=$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1 }') + if [[ -z "$session" ]]; then + echo "no session found for user logind-test-user" >&2 + return 1 + fi + + if ! loginctl session-status "$session" | grep -q "Unit: session-${session}\.scope"; then + echo "cannot find scope unit for session $session" >&2 + return 1 + fi + + leader_pid=$(loginctl session-status "$session" | awk '$1 == "Leader:" { print $2 }') + if [[ -z "$leader_pid" ]]; then + echo "cannot found leader process for session $session" >&2 + return 1 + fi + + # cgroup v1: "1:name=systemd:/user.slice/..."; unified hierarchy: "0::/user.slice" + if ! grep -q -E '(name=systemd|^0:):.*session.*scope' /proc/"$leader_pid"/cgroup; then + echo "FAIL: process $leader_pid is not in the session cgroup" >&2 + cat /proc/self/cgroup + return 1 + fi +) + +create_session() { + # login with the test user to start a session + mkdir -p /run/systemd/system/getty@tty2.service.d + cat >/run/systemd/system/getty@tty2.service.d/override.conf <<EOF +[Service] +Type=simple +ExecStart= +ExecStart=-/sbin/agetty --autologin logind-test-user --noclear %I $TERM +Restart=no +EOF + systemctl daemon-reload + + systemctl restart getty@tty2.service + + # check session + for i in {1..30}; do + (( i > 1 )) && sleep 1 + check_session && break + done + check_session + assert_eq "$(loginctl --no-legend | awk '$3=="logind-test-user" { print $5 }')" "tty2" +} + +testcase_sanity_check() { + # Exercise basic loginctl options + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + trap cleanup_session RETURN + create_session + + # Run most of the loginctl commands from a user session to make + # the seat/session autodetection work-ish + systemd-run --user --pipe --wait -M "logind-test-user@.host" bash -eux <<\EOF + loginctl list-sessions + loginctl session-status + loginctl show-session + loginctl show-session -P DelayInhibited + + # We're not in the same session scope, so in this case we need to specify + # the session ID explicitly + session=$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1; exit; }') + loginctl kill-session --signal=SIGCONT "$session" + # FIXME(?) + #loginctl kill-session --signal=SIGCONT --kill-whom=leader "$session" + + loginctl list-users + loginctl user-status + loginctl show-user -a + loginctl show-user -P IdleAction + loginctl kill-user --signal=SIGCONT "" + + loginctl list-seats + loginctl seat-status + loginctl show-seat + loginctl show-seat -P IdleActionUSec +EOF + + # Requires root privileges + loginctl lock-sessions + loginctl unlock-sessions + loginctl flush-devices +} + +testcase_session() { + local dev + + if systemd-detect-virt --quiet --container; then + echo "Skipping ACL tests in container" + return + fi + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + trap teardown_session RETURN + + create_session + + # scsi_debug should not be loaded yet + if [[ -d /sys/bus/pseudo/drivers/scsi_debug ]]; then + echo "scsi_debug module is already loaded." >&2 + exit 1 + fi + + # we use scsi_debug to create new devices which we can put ACLs on + # tell udev about the tagging, so that logind can pick it up + mkdir -p /run/udev/rules.d + cat >/run/udev/rules.d/70-logindtest-scsi_debug-user.rules <<EOF +SUBSYSTEM=="block", ATTRS{model}=="scsi_debug*", TAG+="uaccess" +EOF + udevadm control --reload + + # coldplug: logind started with existing device + systemctl stop systemd-logind.service + modprobe scsi_debug + timeout 30 bash -c 'until ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block 2>/dev/null; do sleep 1; done' + dev=/dev/$(ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block 2>/dev/null) + if [[ ! -b "$dev" ]]; then + echo "cannot find suitable scsi block device" >&2 + exit 1 + fi + udevadm settle + udevadm info "$dev" + + # trigger logind and activate session + loginctl activate "$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1 }')" + + # check ACL + sleep 1 + assert_in "user:logind-test-user:rw-" "$(getfacl -p "$dev")" + + # hotplug: new device appears while logind is running + rmmod scsi_debug + modprobe scsi_debug + timeout 30 bash -c 'until ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block 2>/dev/null; do sleep 1; done' + dev=/dev/$(ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block 2>/dev/null) + if [[ ! -b "$dev" ]]; then + echo "cannot find suitable scsi block device" >&2 + exit 1 + fi + udevadm settle + + # check ACL + sleep 1 + assert_in "user:logind-test-user:rw-" "$(getfacl -p "$dev")" +} + +teardown_lock_idle_action() ( + set +eux + + rm -f /run/systemd/logind.conf.d/idle-action-lock.conf + systemctl restart systemd-logind.service + + cleanup_session + + return 0 +) + +testcase_lock_idle_action() { + local ts + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + if loginctl --no-legend | grep -q logind-test-user; then + echo >&2 "Session of the 'logind-test-user' is already present." + exit 1 + fi + + trap teardown_lock_idle_action RETURN + + create_session + + ts="$(date '+%H:%M:%S')" + + mkdir -p /run/systemd/logind.conf.d + cat >/run/systemd/logind.conf.d/idle-action-lock.conf <<EOF +[Login] +IdleAction=lock +IdleActionSec=1s +EOF + systemctl restart systemd-logind.service + + # Wait for 35s, in that interval all sessions should have become idle + # and "Lock" signal should have been sent out. Then we wrote to tty to make + # session active again and next we slept for another 35s so sessions have + # become idle again. 'Lock' signal is sent out for each session, we have at + # least one session, so minimum of 2 "Lock" signals must have been sent. + timeout 35 bash -c "while [[ \"\$(journalctl -b -u systemd-logind.service --since=$ts | grep -c 'Sent message type=signal .* member=Lock')\" -lt 1 ]]; do sleep 1; done" + + # Wakeup + touch /dev/tty2 + + # Wait again + timeout 35 bash -c "while [[ \"\$(journalctl -b -u systemd-logind.service --since=$ts | grep -c 'Sent message type=signal .* member=Lock')\" -lt 2 ]]; do sleep 1; done" + + if [[ "$(journalctl -b -u systemd-logind.service --since="$ts" | grep -c 'System idle. Will be locked now.')" -lt 2 ]]; then + echo >&2 "System haven't entered idle state at least 2 times." + exit 1 + fi +} + +testcase_session_properties() { + local s + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + trap cleanup_session RETURN + create_session + + s=$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $1 }') + /usr/lib/systemd/tests/unit-tests/manual/test-session-properties "/org/freedesktop/login1/session/_3${s?}" /dev/tty2 +} + +testcase_list_users_sessions_seats() { + local session seat + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + trap cleanup_session RETURN + create_session + + # Activate the session + loginctl activate "$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1 }')" + + session=$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $1 }') + : check that we got a valid session id + busctl get-property org.freedesktop.login1 "/org/freedesktop/login1/session/_3${session?}" org.freedesktop.login1.Session Id + assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $2 }')" "$(id -ru logind-test-user)" + seat=$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $4 }') + assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $5 }')" tty2 + assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $6 }')" active + assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $7 }')" no + assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $8 }')" '-' + + loginctl list-seats --no-legend | grep -Fwq "${seat?}" + + assert_eq "$(loginctl list-users --no-legend | awk '$2 == "logind-test-user" { print $1 }')" "$(id -ru logind-test-user)" + assert_eq "$(loginctl list-users --no-legend | awk '$2 == "logind-test-user" { print $3 }')" no + assert_eq "$(loginctl list-users --no-legend | awk '$2 == "logind-test-user" { print $4 }')" active + + loginctl enable-linger logind-test-user + assert_eq "$(loginctl list-users --no-legend | awk '$2 == "logind-test-user" { print $3 }')" yes + + for s in $(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $1 }'); do + loginctl terminate-session "$s" + done + if ! timeout 30 bash -c "while loginctl --no-legend | grep -q logind-test-user; do sleep 1; done"; then + echo "WARNING: session for logind-test-user still active, ignoring." + return + fi + + assert_eq "$(loginctl list-users --no-legend | awk '$2 == "logind-test-user" { print $4 }')" lingering +} + +teardown_stop_idle_session() ( + set +eux + + rm -f /run/systemd/logind.conf.d/stop-idle-session.conf + systemctl restart systemd-logind.service + + cleanup_session +) + +testcase_stop_idle_session() { + local id ts + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + create_session + trap teardown_stop_idle_session RETURN + + id="$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1; }')" + ts="$(date '+%H:%M:%S')" + + mkdir -p /run/systemd/logind.conf.d + cat >/run/systemd/logind.conf.d/stop-idle-session.conf <<EOF +[Login] +StopIdleSessionSec=2s +EOF + systemctl restart systemd-logind.service + sleep 5 + + assert_eq "$(journalctl -b -u systemd-logind.service --since="$ts" --grep "Session \"$id\" of user \"logind-test-user\" is idle, stopping." | wc -l)" 1 + assert_eq "$(loginctl --no-legend | grep -c "logind-test-user")" 0 +} + +testcase_ambient_caps() { + local PAMSERVICE TRANSIENTUNIT SCRIPT + + # Verify that pam_systemd works and assigns ambient caps as it should + + if ! grep -q 'CapAmb:' /proc/self/status ; then + echo "ambient caps not available, skipping test." >&2 + return + fi + + typeset -i BND MASK + + # Get PID 1's bounding set + BND="0x$(grep 'CapBnd:' /proc/1/status | cut -d: -f2 | tr -d '[:space:]')" + + # CAP_CHOWN | CAP_KILL + MASK=$(((1 << 0) | (1 << 5))) + + if [ $((BND & MASK)) -ne "$MASK" ] ; then + echo "CAP_CHOWN or CAP_KILL not available in bounding set, skipping test." >&2 + return + fi + + PAMSERVICE="pamserv$RANDOM" + TRANSIENTUNIT="capwakealarm$RANDOM.service" + SCRIPT="/tmp/capwakealarm$RANDOM.sh" + + cat > /etc/pam.d/"$PAMSERVICE" <<EOF +auth sufficient pam_unix.so +auth required pam_deny.so +account sufficient pam_unix.so +account required pam_permit.so +session optional pam_systemd.so default-capability-ambient-set=CAP_CHOWN,CAP_KILL debug +session required pam_unix.so +EOF + + cat > "$SCRIPT" <<'EOF' +#!/bin/bash +set -ex +typeset -i AMB MASK +AMB="0x$(grep 'CapAmb:' /proc/self/status | cut -d: -f2 | tr -d '[:space:]')" +MASK=$(((1 << 0) | (1 << 5))) +test "$AMB" -eq "$MASK" +EOF + + chmod +x "$SCRIPT" + + systemd-run -u "$TRANSIENTUNIT" -p PAMName="$PAMSERVICE" -p Type=oneshot -p User=logind-test-user -p StandardError=tty "$SCRIPT" + + rm -f "$SCRIPT" "$PAMSERVICE" +} + +setup_test_user +test_enable_debug +run_testcases + +touch /testok diff --git a/test/units/testsuite-36.service b/test/units/testsuite-36.service new file mode 100644 index 0000000..5746dc1 --- /dev/null +++ b/test/units/testsuite-36.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-36-NUMAPOLICY + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-36.sh b/test/units/testsuite-36.sh new file mode 100755 index 0000000..8a53b98 --- /dev/null +++ b/test/units/testsuite-36.sh @@ -0,0 +1,352 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck disable=SC2317 +at_exit() { + # shellcheck disable=SC2181 + if [[ $? -ne 0 ]]; then + # We're exiting with a non-zero EC, let's dump test artifacts + # for easier debugging + [[ -v straceLog && -f "$straceLog" ]] && cat "$straceLog" + [[ -v journalLog && -f "$journalLog" ]] && cat "$journalLog" + fi +} + +trap at_exit EXIT + +systemd-analyze log-level debug +systemd-analyze log-target journal + +# Log files +straceLog='strace.log' +journalLog='journal.log' + +# Systemd config files +testUnit='numa-test.service' +testUnitFile="/run/systemd/system/$testUnit" +testUnitNUMAConf="$testUnitFile.d/numa.conf" + +# Sleep constants (we should probably figure out something better but nothing comes to mind) +sleepAfterStart=1 + +# Journal cursor for easier navigation +journalCursorFile="jounalCursorFile" + +startStrace() { + coproc strace -qq -p 1 -o "$straceLog" -e set_mempolicy -s 1024 ${1:+"$1"} + # Wait for strace to properly "initialize", i.e. until PID 1 has the TracerPid + # field set to the current strace's PID + until awk -v spid="$COPROC_PID" '/^TracerPid:/ {exit !($2 == spid);}' /proc/1/status; do sleep 0.1; done +} + +stopStrace() { + [[ -v COPROC_PID ]] || return + + local PID=$COPROC_PID + kill -s TERM "$PID" + # Make sure the strace process is indeed dead + while kill -0 "$PID" 2>/dev/null; do sleep 0.1; done +} + +startJournalctl() { + : >"$journalCursorFile" + # Save journal's cursor for later navigation + journalctl --no-pager --cursor-file="$journalCursorFile" -n0 -ocat +} + +stopJournalctl() { + local unit="${1:-init.scope}" + # Using journalctl --sync should be better than using SIGRTMIN+1, as + # the --sync wait until the synchronization is complete + echo "Force journald to write all queued messages" + journalctl --sync + journalctl -u "$unit" --cursor-file="$journalCursorFile" >"$journalLog" +} + +checkNUMA() { + # NUMA enabled system should have at least NUMA node0 + test -e /sys/devices/system/node/node0 +} + +writePID1NUMAPolicy() { + cat >"$confDir/numa.conf" <<EOF +[Manager] +NUMAPolicy=${1:?} +NUMAMask=${2:-""} +EOF +} + +writeTestUnit() { + mkdir -p "$testUnitFile.d/" + printf "[Service]\nExecStart=/bin/sleep 3600\n" >"$testUnitFile" +} + +writeTestUnitNUMAPolicy() { + cat >"$testUnitNUMAConf" <<EOF +[Service] +NUMAPolicy=${1:?} +NUMAMask=${2:-""} +EOF + systemctl daemon-reload +} + +pid1ReloadWithStrace() { + startStrace + systemctl daemon-reload + sleep $sleepAfterStart + stopStrace +} + +pid1ReloadWithJournal() { + startJournalctl + systemctl daemon-reload + stopJournalctl +} + +pid1StartUnitWithStrace() { + startStrace '-f' + systemctl start "${1:?}" + sleep $sleepAfterStart + stopStrace +} + +pid1StartUnitWithJournal() { + startJournalctl + systemctl start "${1:?}" + sleep $sleepAfterStart + stopJournalctl +} + +pid1StopUnit() { + systemctl stop "${1:?}" +} + +systemctlCheckNUMAProperties() { + local UNIT_NAME="${1:?}" + local NUMA_POLICY="${2:?}" + local NUMA_MASK="${3:-""}" + local LOGFILE + + LOGFILE="$(mktemp)" + + systemctl show -p NUMAPolicy "$UNIT_NAME" >"$LOGFILE" + grep "NUMAPolicy=$NUMA_POLICY" "$LOGFILE" + + : >"$LOGFILE" + + if [ -n "$NUMA_MASK" ]; then + systemctl show -p NUMAMask "$UNIT_NAME" >"$LOGFILE" + grep "NUMAMask=$NUMA_MASK" "$LOGFILE" + fi +} + +writeTestUnit + +# Create systemd config drop-in directory +confDir="/run/systemd/system.conf.d/" +mkdir -p "$confDir" + +if ! checkNUMA; then + echo >&2 "NUMA is not supported on this machine, switching to a simple sanity check" + + echo "PID1 NUMAPolicy=default && NUMAMask=0 check without NUMA support" + writePID1NUMAPolicy "default" "0" + startJournalctl + systemctl daemon-reload + stopJournalctl + grep "NUMA support not available, ignoring" "$journalLog" + + echo "systemd-run NUMAPolicy=default && NUMAMask=0 check without NUMA support" + runUnit='numa-systemd-run-test.service' + startJournalctl + systemd-run -p NUMAPolicy=default -p NUMAMask=0 --unit "$runUnit" sleep 1000 + sleep $sleepAfterStart + pid1StopUnit "$runUnit" + stopJournalctl "$runUnit" + grep "NUMA support not available, ignoring" "$journalLog" + +else + echo "PID1 NUMAPolicy support - Default policy w/o mask" + writePID1NUMAPolicy "default" + pid1ReloadWithStrace + # Kernel requires that nodemask argument is set to NULL when setting default policy + grep "set_mempolicy(MPOL_DEFAULT, NULL" "$straceLog" + + echo "PID1 NUMAPolicy support - Default policy w/ mask" + writePID1NUMAPolicy "default" "0" + pid1ReloadWithStrace + grep "set_mempolicy(MPOL_DEFAULT, NULL" "$straceLog" + + echo "PID1 NUMAPolicy support - Bind policy w/o mask" + writePID1NUMAPolicy "bind" + pid1ReloadWithJournal + grep "Failed to set NUMA memory policy, ignoring: Invalid argument" "$journalLog" + + echo "PID1 NUMAPolicy support - Bind policy w/ mask" + writePID1NUMAPolicy "bind" "0" + pid1ReloadWithStrace + grep -P "set_mempolicy\(MPOL_BIND, \[0x0*1\]" "$straceLog" + + echo "PID1 NUMAPolicy support - Interleave policy w/o mask" + writePID1NUMAPolicy "interleave" + pid1ReloadWithJournal + grep "Failed to set NUMA memory policy, ignoring: Invalid argument" "$journalLog" + + echo "PID1 NUMAPolicy support - Interleave policy w/ mask" + writePID1NUMAPolicy "interleave" "0" + pid1ReloadWithStrace + grep -P "set_mempolicy\(MPOL_INTERLEAVE, \[0x0*1\]" "$straceLog" + + echo "PID1 NUMAPolicy support - Preferred policy w/o mask" + writePID1NUMAPolicy "preferred" + pid1ReloadWithJournal + # Preferred policy with empty node mask is actually allowed and should reset allocation policy to default + grep "Failed to set NUMA memory policy, ignoring: Invalid argument" "$journalLog" && { echo >&2 "unexpected pass"; exit 1; } + + echo "PID1 NUMAPolicy support - Preferred policy w/ mask" + writePID1NUMAPolicy "preferred" "0" + pid1ReloadWithStrace + grep -P "set_mempolicy\(MPOL_PREFERRED, \[0x0*1\]" "$straceLog" + + echo "PID1 NUMAPolicy support - Local policy w/o mask" + writePID1NUMAPolicy "local" + pid1ReloadWithStrace + # Kernel requires that nodemask argument is set to NULL when setting default policy + # The unpatched versions of strace don't recognize the MPOL_LOCAL constant and + # return a numerical constant instead (with a comment): + # set_mempolicy(0x4 /* MPOL_??? */, NULL, 0) = 0 + # Let's cover this scenario as well + grep -E "set_mempolicy\((MPOL_LOCAL|0x4 [^,]*), NULL" "$straceLog" + + echo "PID1 NUMAPolicy support - Local policy w/ mask" + writePID1NUMAPolicy "local" "0" + pid1ReloadWithStrace + grep -E "set_mempolicy\((MPOL_LOCAL|0x4 [^,]*), NULL" "$straceLog" + + echo "Unit file NUMAPolicy support - Default policy w/o mask" + writeTestUnitNUMAPolicy "default" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "default" + pid1StopUnit "$testUnit" + grep "set_mempolicy(MPOL_DEFAULT, NULL" "$straceLog" + + echo "Unit file NUMAPolicy support - Default policy w/ mask" + writeTestUnitNUMAPolicy "default" "0" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "default" "0" + pid1StopUnit $testUnit + # Mask must be ignored + grep "set_mempolicy(MPOL_DEFAULT, NULL" "$straceLog" + + echo "Unit file NUMAPolicy support - Bind policy w/o mask" + writeTestUnitNUMAPolicy "bind" + pid1StartUnitWithJournal "$testUnit" + pid1StopUnit "$testUnit" + [[ $(systemctl show "$testUnit" -P ExecMainStatus) == "242" ]] + + echo "Unit file NUMAPolicy support - Bind policy w/ mask" + writeTestUnitNUMAPolicy "bind" "0" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "bind" "0" + pid1StopUnit "$testUnit" + grep -P "set_mempolicy\(MPOL_BIND, \[0x0*1\]" "$straceLog" + + echo "Unit file NUMAPolicy support - Interleave policy w/o mask" + writeTestUnitNUMAPolicy "interleave" + pid1StartUnitWithStrace "$testUnit" + pid1StopUnit "$testUnit" + [[ $(systemctl show "$testUnit" -P ExecMainStatus) == "242" ]] + + echo "Unit file NUMAPolicy support - Interleave policy w/ mask" + writeTestUnitNUMAPolicy "interleave" "0" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "interleave" "0" + pid1StopUnit "$testUnit" + grep -P "set_mempolicy\(MPOL_INTERLEAVE, \[0x0*1\]" "$straceLog" + + echo "Unit file NUMAPolicy support - Preferred policy w/o mask" + writeTestUnitNUMAPolicy "preferred" + pid1StartUnitWithJournal "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "preferred" + pid1StopUnit "$testUnit" + [[ $(systemctl show "$testUnit" -P ExecMainStatus) == "242" ]] && { echo >&2 "unexpected pass"; exit 1; } + + echo "Unit file NUMAPolicy support - Preferred policy w/ mask" + writeTestUnitNUMAPolicy "preferred" "0" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "preferred" "0" + pid1StopUnit "$testUnit" + grep -P "set_mempolicy\(MPOL_PREFERRED, \[0x0*1\]" "$straceLog" + + echo "Unit file NUMAPolicy support - Local policy w/o mask" + writeTestUnitNUMAPolicy "local" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "local" + pid1StopUnit "$testUnit" + grep -E "set_mempolicy\((MPOL_LOCAL|0x4 [^,]*), NULL" "$straceLog" + + echo "Unit file NUMAPolicy support - Local policy w/ mask" + writeTestUnitNUMAPolicy "local" "0" + pid1StartUnitWithStrace "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "local" "0" + pid1StopUnit "$testUnit" + # Mask must be ignored + grep -E "set_mempolicy\((MPOL_LOCAL|0x4 [^,]*), NULL" "$straceLog" + + echo "Unit file CPUAffinity=NUMA support" + writeTestUnitNUMAPolicy "bind" "0" + echo "CPUAffinity=numa" >>"$testUnitNUMAConf" + systemctl daemon-reload + systemctl start "$testUnit" + systemctlCheckNUMAProperties "$testUnit" "bind" "0" + cpulist="$(cat /sys/devices/system/node/node0/cpulist)" + affinity_systemd="$(systemctl show --value -p CPUAffinity "$testUnit")" + [ "$cpulist" = "$affinity_systemd" ] + pid1StopUnit "$testUnit" + + echo "systemd-run NUMAPolicy support" + runUnit='numa-systemd-run-test.service' + + systemd-run -p NUMAPolicy=default --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "default" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=default -p NUMAMask=0 --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "default" "" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=bind -p NUMAMask=0 --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "bind" "0" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=interleave -p NUMAMask=0 --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "interleave" "0" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=preferred -p NUMAMask=0 --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "preferred" "0" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=local --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "local" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=local -p NUMAMask=0 --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "local" "" + pid1StopUnit "$runUnit" + + systemd-run -p NUMAPolicy=local -p NUMAMask=0 -p CPUAffinity=numa --unit "$runUnit" sleep 1000 + systemctlCheckNUMAProperties "$runUnit" "local" "" + systemctl cat "$runUnit" | grep -q 'CPUAffinity=numa' + pid1StopUnit "$runUnit" +fi + +# Cleanup +rm -rf "$confDir" +systemctl daemon-reload + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-38-sleep.service b/test/units/testsuite-38-sleep.service new file mode 100644 index 0000000..c116c80 --- /dev/null +++ b/test/units/testsuite-38-sleep.service @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Service] +ExecStart=/bin/sleep 3600 diff --git a/test/units/testsuite-38.service b/test/units/testsuite-38.service new file mode 100644 index 0000000..ac77836 --- /dev/null +++ b/test/units/testsuite-38.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-38-FREEZER + +[Service] +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-38.sh b/test/units/testsuite-38.sh new file mode 100755 index 0000000..5fc87fc --- /dev/null +++ b/test/units/testsuite-38.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2317 +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +systemd-analyze log-level debug + +unit=testsuite-38-sleep.service + +start_test_service() { + systemctl daemon-reload + systemctl start "${unit}" +} + +dbus_freeze() { + local name object_path suffix + + suffix="${1##*.}" + name="${1%".$suffix"}" + object_path="/org/freedesktop/systemd1/unit/${name//-/_2d}_2e${suffix}" + + busctl call \ + org.freedesktop.systemd1 \ + "${object_path}" \ + org.freedesktop.systemd1.Unit \ + Freeze +} + +dbus_thaw() { + local name object_path suffix + + suffix="${1##*.}" + name="${1%".$suffix"}" + object_path="/org/freedesktop/systemd1/unit/${name//-/_2d}_2e${suffix}" + + busctl call \ + org.freedesktop.systemd1 \ + "${object_path}" \ + org.freedesktop.systemd1.Unit \ + Thaw +} + +dbus_freeze_unit() { + busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + FreezeUnit \ + s \ + "$1" +} + +dbus_thaw_unit() { + busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + ThawUnit \ + s \ + "$1" +} + +dbus_can_freeze() { + local name object_path suffix + + suffix="${1##*.}" + name="${1%".$suffix"}" + object_path="/org/freedesktop/systemd1/unit/${name//-/_2d}_2e${suffix}" + + busctl get-property \ + org.freedesktop.systemd1 \ + "${object_path}" \ + org.freedesktop.systemd1.Unit \ + CanFreeze +} + +check_freezer_state() { + local name object_path suffix + + suffix="${1##*.}" + name="${1%".$suffix"}" + object_path="/org/freedesktop/systemd1/unit/${name//-/_2d}_2e${suffix}" + + for _ in {0..10}; do + state=$(busctl get-property \ + org.freedesktop.systemd1 \ + "${object_path}" \ + org.freedesktop.systemd1.Unit \ + FreezerState | cut -d " " -f2 | tr -d '"') + + # Ignore the intermediate freezing & thawing states in case we check + # the unit state too quickly + [[ "$state" =~ ^(freezing|thawing)$ ]] || break + sleep .5 + done + + [ "$state" = "$2" ] || { + echo "error: unexpected freezer state, expected: $2, actual: $state" >&2 + exit 1 + } +} + +check_cgroup_state() { + grep -q "frozen $2" /sys/fs/cgroup/system.slice/"$1"/cgroup.events +} + +testcase_dbus_api() { + echo "Test that DBus API works:" + echo -n " - Freeze(): " + dbus_freeze "${unit}" + check_freezer_state "${unit}" "frozen" + check_cgroup_state "$unit" 1 + echo "[ OK ]" + + echo -n " - Thaw(): " + dbus_thaw "${unit}" + check_freezer_state "${unit}" "running" + check_cgroup_state "$unit" 0 + echo "[ OK ]" + + echo -n " - FreezeUnit(): " + dbus_freeze_unit "${unit}" + check_freezer_state "${unit}" "frozen" + check_cgroup_state "$unit" 1 + echo "[ OK ]" + + echo -n " - ThawUnit(): " + dbus_thaw_unit "${unit}" + check_freezer_state "${unit}" "running" + check_cgroup_state "$unit" 0 + echo "[ OK ]" + + echo -n " - CanFreeze(): " + output=$(dbus_can_freeze "${unit}") + [ "$output" = "b true" ] + echo "[ OK ]" + + echo +} + +testcase_jobs() { + local pid_before= + local pid_after= + echo "Test that it is possible to apply jobs on frozen units:" + + systemctl start "${unit}" + dbus_freeze "${unit}" + check_freezer_state "${unit}" "frozen" + + echo -n " - restart: " + pid_before=$(systemctl show -p MainPID "${unit}" --value) + systemctl restart "${unit}" + pid_after=$(systemctl show -p MainPID "${unit}" --value) + [ "$pid_before" != "$pid_after" ] && echo "[ OK ]" + + dbus_freeze "${unit}" + check_freezer_state "${unit}" "frozen" + + echo -n " - stop: " + timeout 5s systemctl stop "${unit}" + echo "[ OK ]" + + echo +} + +testcase_systemctl() { + echo "Test that systemctl freeze/thaw verbs:" + + systemctl start "$unit" + + echo -n " - freeze: " + systemctl freeze "$unit" + check_freezer_state "${unit}" "frozen" + check_cgroup_state "$unit" 1 + # Freezing already frozen unit should be NOP and return quickly + timeout 3s systemctl freeze "$unit" + echo "[ OK ]" + + echo -n " - thaw: " + systemctl thaw "$unit" + check_freezer_state "${unit}" "running" + check_cgroup_state "$unit" 0 + # Likewise thawing already running unit shouldn't block + timeout 3s systemctl thaw "$unit" + echo "[ OK ]" + + systemctl stop "$unit" + + echo +} + +testcase_systemctl_show() { + echo "Test systemctl show integration:" + + systemctl start "$unit" + + echo -n " - FreezerState property: " + state=$(systemctl show -p FreezerState --value "$unit") + [ "$state" = "running" ] + systemctl freeze "$unit" + state=$(systemctl show -p FreezerState --value "$unit") + [ "$state" = "frozen" ] + systemctl thaw "$unit" + echo "[ OK ]" + + echo -n " - CanFreeze property: " + state=$(systemctl show -p CanFreeze --value "$unit") + [ "$state" = "yes" ] + echo "[ OK ]" + + systemctl stop "$unit" + echo +} + +testcase_recursive() { + local slice="bar.slice" + local unit="baz.service" + + systemd-run --unit "$unit" --slice "$slice" sleep 3600 >/dev/null 2>&1 + + echo "Test recursive freezing:" + + echo -n " - freeze: " + systemctl freeze "$slice" + check_freezer_state "${slice}" "frozen" + check_freezer_state "${unit}" "frozen" + grep -q "frozen 1" /sys/fs/cgroup/"${slice}"/cgroup.events + grep -q "frozen 1" /sys/fs/cgroup/"${slice}"/"${unit}"/cgroup.events + echo "[ OK ]" + + echo -n " - thaw: " + systemctl thaw "$slice" + check_freezer_state "${unit}" "running" + check_freezer_state "${slice}" "running" + grep -q "frozen 0" /sys/fs/cgroup/"${slice}"/cgroup.events + grep -q "frozen 0" /sys/fs/cgroup/"${slice}"/"${unit}"/cgroup.events + echo "[ OK ]" + + systemctl stop "$unit" + systemctl stop "$slice" + + echo +} + +testcase_preserve_state() { + local slice="bar.slice" + local unit="baz.service" + + systemd-run --unit "$unit" --slice "$slice" sleep 3600 >/dev/null 2>&1 + + echo "Test that freezer state is preserved when recursive freezing is initiated from outside (e.g. by manager up the tree):" + + echo -n " - freeze from outside: " + echo 1 >/sys/fs/cgroup/"${slice}"/cgroup.freeze + # Give kernel some time to freeze the slice + sleep 1 + + # Our state should not be affected + check_freezer_state "${slice}" "running" + check_freezer_state "${unit}" "running" + + # However actual kernel state should be frozen + grep -q "frozen 1" /sys/fs/cgroup/"${slice}"/cgroup.events + grep -q "frozen 1" /sys/fs/cgroup/"${slice}"/"${unit}"/cgroup.events + echo "[ OK ]" + + echo -n " - thaw from outside: " + echo 0 >/sys/fs/cgroup/"${slice}"/cgroup.freeze + sleep 1 + + check_freezer_state "${unit}" "running" + check_freezer_state "${slice}" "running" + grep -q "frozen 0" /sys/fs/cgroup/"${slice}"/cgroup.events + grep -q "frozen 0" /sys/fs/cgroup/"${slice}"/"${unit}"/cgroup.events + echo "[ OK ]" + + echo -n " - thaw from outside while inner service is frozen: " + systemctl freeze "$unit" + check_freezer_state "${unit}" "frozen" + echo 1 >/sys/fs/cgroup/"${slice}"/cgroup.freeze + echo 0 >/sys/fs/cgroup/"${slice}"/cgroup.freeze + check_freezer_state "${slice}" "running" + check_freezer_state "${unit}" "frozen" + echo "[ OK ]" + + systemctl stop "$unit" + systemctl stop "$slice" + + echo +} + +if [[ -e /sys/fs/cgroup/system.slice/cgroup.freeze ]]; then + start_test_service + run_testcases +fi + +touch /testok diff --git a/test/units/testsuite-43.service b/test/units/testsuite-43.service new file mode 100644 index 0000000..e36afea --- /dev/null +++ b/test/units/testsuite-43.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-43-PRIVATEUSER-UNPRIV +After=systemd-logind.service user@4711.service +Wants=user@4711.service + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-43.sh b/test/units/testsuite-43.sh new file mode 100755 index 0000000..4f31a33 --- /dev/null +++ b/test/units/testsuite-43.sh @@ -0,0 +1,143 @@ +#!/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 + +if [[ "$(sysctl -ne kernel.apparmor_restrict_unprivileged_userns)" -eq 1 ]]; then + echo "Cannot create unprivileged user namespaces" >/skipped + exit 0 +fi + +systemd-analyze log-level debug + +runas testuser systemd-run --wait --user --unit=test-private-users \ + -p PrivateUsers=yes -P echo hello + +runas testuser systemctl --user log-level debug + +runas testuser systemd-run --wait --user --unit=test-private-tmp-innerfile \ + -p PrivateTmp=yes \ + -P touch /tmp/innerfile.txt +# File should not exist outside the job's tmp directory. +test ! -e /tmp/innerfile.txt + +touch /tmp/outerfile.txt +# File should not appear in unit's private tmp. +runas testuser systemd-run --wait --user --unit=test-private-tmp-outerfile \ + -p PrivateTmp=yes \ + -P test ! -e /tmp/outerfile.txt + +# Confirm that creating a file in home works +runas testuser systemd-run --wait --user --unit=test-unprotected-home \ + -P touch /home/testuser/works.txt +test -e /home/testuser/works.txt + +# Confirm that creating a file in home is blocked under read-only +(! runas testuser systemd-run --wait --user --unit=test-protect-home-read-only \ + -p ProtectHome=read-only \ + -P bash -c ' + test -e /home/testuser/works.txt || exit 10 + touch /home/testuser/blocked.txt && exit 11 + ') +test ! -e /home/testuser/blocked.txt + +# Check that tmpfs hides the whole directory +runas testuser systemd-run --wait --user --unit=test-protect-home-tmpfs \ + -p ProtectHome=tmpfs \ + -P test ! -e /home/testuser + +# Confirm that home, /root, and /run/user are inaccessible under "yes" +# shellcheck disable=SC2016 +runas testuser systemd-run --wait --user --unit=test-protect-home-yes \ + -p ProtectHome=yes \ + -P bash -c ' + test "$(stat -c %a /home)" = "0" + test "$(stat -c %a /root)" = "0" + test "$(stat -c %a /run/user)" = "0" + ' + +# Confirm we cannot change groups because we only have one mapping in the user +# namespace (no CAP_SETGID in the parent namespace to write the additional +# mapping of the user supplied group and thus cannot change groups to an +# unmapped group ID) +(! runas testuser systemd-run --wait --user --unit=test-group-fail \ + -p PrivateUsers=yes -p Group=daemon \ + -P true) + +# Check that with a new user namespace we can bind mount +# files and use a different root directory +runas testuser systemd-run --wait --user --unit=test-bind-mount \ + -p BindPaths=/dev/null:/etc/os-release \ + test ! -s /etc/os-release + +runas testuser systemd-run --wait --user --unit=test-read-write \ + -p ReadOnlyPaths=/ \ + -p ReadWritePaths="/var /run /tmp" \ + -p NoExecPaths=/ -p ExecPaths=/usr \ + test ! -w /etc/os-release + +runas testuser systemd-run --wait --user --unit=test-caps \ + -p PrivateUsers=yes -p AmbientCapabilities=CAP_SYS_ADMIN \ + -p CapabilityBoundingSet=CAP_SYS_ADMIN \ + test -s /etc/os-release + +runas testuser systemd-run --wait --user --unit=test-devices \ + -p PrivateDevices=yes -p PrivateIPC=yes \ + sh -c "ls -1 /dev/ | wc -l | grep -q -F 18" + +# Same check as test/test-execute/exec-privatenetwork-yes.service +runas testuser systemd-run --wait --user --unit=test-network \ + -p PrivateNetwork=yes \ + /bin/sh -x -c '! ip link | grep -E "^[0-9]+: " | grep -Ev ": (lo|(erspan|gre|gretap|ip_vti|ip6_vti|ip6gre|ip6tnl|sit|tunl)0@.*):"' + +(! runas testuser systemd-run --wait --user --unit=test-hostname \ + -p ProtectHostname=yes \ + hostnamectl hostname foo) + +(! runas testuser systemd-run --wait --user --unit=test-clock \ + -p ProtectClock=yes \ + timedatectl set-time "2012-10-30 18:17:16") + +(! runas testuser systemd-run --wait --user --unit=test-kernel-tunable \ + -p ProtectKernelTunables=yes \ + sh -c "echo 0 >/proc/sys/user/max_user_namespaces") + +(! runas testuser systemd-run --wait --user --unit=test-kernel-mod \ + -p ProtectKernelModules=yes \ + sh -c "modprobe -r overlay && modprobe overlay") + +if sysctl kernel.dmesg_restrict=0; then + (! runas testuser systemd-run --wait --user --unit=test-kernel-log \ + -p ProtectKernelLogs=yes -p LogNamespace=yes \ + dmesg) +fi + +unsquashfs -no-xattrs -d /tmp/img /usr/share/minimal_0.raw +runas testuser systemd-run --wait --user --unit=test-root-dir \ + -p RootDirectory=/tmp/img \ + grep MARKER=1 /etc/os-release + +mkdir /tmp/img_bind +mount --bind /tmp/img /tmp/img_bind +runas testuser systemd-run --wait --user --unit=test-root-dir-bind \ + -p RootDirectory=/tmp/img_bind -p MountFlags=private \ + grep MARKER=1 /etc/os-release +umount /tmp/img_bind + +# Unprivileged overlayfs was added to Linux 5.11, so try to detect it first +mkdir -p /tmp/a /tmp/b /tmp/c +if unshare --mount --user --map-root-user mount -t overlay overlay /tmp/c -o lowerdir=/tmp/a:/tmp/b; then + unsquashfs -no-xattrs -d /tmp/app2 /usr/share/app1.raw + runas testuser systemd-run --wait --user --unit=test-extension-dir \ + -p ExtensionDirectories=/tmp/app2 \ + -p TemporaryFileSystem=/run -p RootDirectory=/tmp/img \ + -p MountAPIVFS=yes \ + grep PORTABLE_PREFIXES=app1 /usr/lib/extension-release.d/extension-release.app2 +fi + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-44.service b/test/units/testsuite-44.service new file mode 100644 index 0000000..4dffdea --- /dev/null +++ b/test/units/testsuite-44.service @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TESTSUITE-44-LOG-NAMESPACE +Before=getty-pre.target +Wants=getty-pre.target +Wants=systemd-journald@foobar.socket systemd-journald-varlink@foobar.socket +After=systemd-journald@foobar.socket systemd-journald-varlink@foobar.socket + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-44.sh b/test/units/testsuite-44.sh new file mode 100755 index 0000000..fbd4ae6 --- /dev/null +++ b/test/units/testsuite-44.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux + +systemd-analyze log-level debug + +systemd-run --wait -p LogNamespace=foobar echo "hello world" + +journalctl --namespace=foobar --sync +journalctl -o cat --namespace=foobar >/tmp/hello-world +journalctl -o cat >/tmp/no-hello-world + +grep "^hello world$" /tmp/hello-world +(! grep "^hello world$" /tmp/no-hello-world) + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-45.service b/test/units/testsuite-45.service new file mode 100644 index 0000000..b16ce99 --- /dev/null +++ b/test/units/testsuite-45.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-45-TIMEDATE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-45.sh b/test/units/testsuite-45.sh new file mode 100755 index 0000000..f124a24 --- /dev/null +++ b/test/units/testsuite-45.sh @@ -0,0 +1,412 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +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 + +testcase_timedatectl() { + timedatectl --no-pager --help + timedatectl --version + + timedatectl + timedatectl --no-ask-password + timedatectl status --machine=testuser@.host + timedatectl status + timedatectl show + timedatectl show --all + timedatectl show -p NTP + timedatectl show -p NTP --value + timedatectl list-timezones + + if ! systemd-detect-virt -qc; then + systemctl enable --runtime --now systemd-timesyncd + timedatectl timesync-status + timedatectl show-timesync + fi +} + +restore_timezone() { + if [[ -f /tmp/timezone.bak ]]; then + mv /tmp/timezone.bak /etc/timezone + else + rm -f /etc/timezone + fi +} + +testcase_timezone() { + local ORIG_TZ= + + # Debian/Ubuntu specific file + if [[ -f /etc/timezone ]]; then + mv /etc/timezone /tmp/timezone.bak + fi + + trap restore_timezone RETURN + + if [[ -L /etc/localtime ]]; then + ORIG_TZ=$(readlink /etc/localtime | sed 's#^.*zoneinfo/##') + echo "original tz: $ORIG_TZ" + fi + + echo 'timedatectl works' + assert_in "Local time:" "$(timedatectl --no-pager)" + + echo 'change timezone' + assert_eq "$(timedatectl --no-pager set-timezone Europe/Kiev 2>&1)" "" + assert_eq "$(readlink /etc/localtime | sed 's#^.*zoneinfo/##')" "Europe/Kiev" + if [[ -f /etc/timezone ]]; then + assert_eq "$(cat /etc/timezone)" "Europe/Kiev" + fi + assert_in "Time zone: Europe/Kiev \(EES*T, \+0[0-9]00\)" "$(timedatectl)" + + if [[ -n "$ORIG_TZ" ]]; then + echo 'reset timezone to original' + assert_eq "$(timedatectl set-timezone "$ORIG_TZ" 2>&1)" "" + assert_eq "$(readlink /etc/localtime | sed 's#^.*zoneinfo/##')" "$ORIG_TZ" + if [[ -f /etc/timezone ]]; then + assert_eq "$(cat /etc/timezone)" "$ORIG_TZ" + fi + fi +} + +restore_adjtime() { + if [[ -e /etc/adjtime.bak ]]; then + mv /etc/adjtime.bak /etc/adjtime + else + rm /etc/adjtime + fi +} + +check_adjtime_not_exist() { + if [[ -e /etc/adjtime ]]; then + echo "/etc/adjtime unexpectedly exists." >&2 + exit 1 + fi +} + +testcase_adjtime() { + # test setting UTC vs. LOCAL in /etc/adjtime + if [[ -e /etc/adjtime ]]; then + mv /etc/adjtime /etc/adjtime.bak + fi + + trap restore_adjtime RETURN + + echo 'no adjtime file' + rm -f /etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + timedatectl set-local-rtc 0 + check_adjtime_not_exist + + echo 'UTC set in adjtime file' + printf '0.0 0 0\n0\nUTC\n' >/etc/adjtime + timedatectl set-local-rtc 0 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +UTC" + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + + echo 'non-zero values in adjtime file' + printf '0.1 123 0\n0\nLOCAL\n' >/etc/adjtime + timedatectl set-local-rtc 0 + assert_eq "$(cat /etc/adjtime)" "0.1 123 0 +0 +UTC" + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.1 123 0 +0 +LOCAL" + + echo 'fourth line adjtime file' + printf '0.0 0 0\n0\nLOCAL\nsomethingelse\n' >/etc/adjtime + timedatectl set-local-rtc 0 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +UTC +somethingelse" + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL +somethingelse" + + echo 'no final newline in adjtime file' + printf '0.0 0 0\n0\nUTC' >/etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + printf '0.0 0 0\n0\nUTC' >/etc/adjtime + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + + echo 'only one line in adjtime file' + printf '0.0 0 0\n' >/etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + printf '0.0 0 0\n' >/etc/adjtime + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + + echo 'only one line in adjtime file, no final newline' + printf '0.0 0 0' >/etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + printf '0.0 0 0' >/etc/adjtime + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + + echo 'only two lines in adjtime file' + printf '0.0 0 0\n0\n' >/etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + printf '0.0 0 0\n0\n' >/etc/adjtime + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + + echo 'only two lines in adjtime file, no final newline' + printf '0.0 0 0\n0' >/etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + printf '0.0 0 0\n0' >/etc/adjtime + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" + + echo 'unknown value in 3rd line of adjtime file' + printf '0.0 0 0\n0\nFOO\n' >/etc/adjtime + timedatectl set-local-rtc 0 + check_adjtime_not_exist + printf '0.0 0 0\n0\nFOO\n' >/etc/adjtime + timedatectl set-local-rtc 1 + assert_eq "$(cat /etc/adjtime)" "0.0 0 0 +0 +LOCAL" +} + +assert_ntp() { + local value="${1:?}" + + for _ in {0..9}; do + [[ "$(busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 org.freedesktop.timedate1 NTP)" == "b $value" ]] && return 0 + sleep .5 + done + + return 1 +} + +assert_timedated_signal() { + local timestamp="${1:?}" + local value="${2:?}" + local args=(-q -n 1 --since="$timestamp" -p info _SYSTEMD_UNIT="busctl-monitor.service") + + journalctl --sync + + for _ in {0..9}; do + if journalctl "${args[@]}" --grep .; then + [[ "$(journalctl "${args[@]}" -o cat | jq -r '.payload.data[1].NTP.data')" == "$value" ]]; + return 0 + fi + + sleep .5 + done + + return 1 +} + +assert_timesyncd_state() { + local state="${1:?}" + + for _ in {0..9}; do + [[ "$(systemctl show systemd-timesyncd.service -P ActiveState)" == "$state" ]] && return 0 + sleep .5 + done + + return 1 +} + +testcase_ntp() { + # This fails due to https://github.com/systemd/systemd/issues/30886 + # but it is too complex and risky to backport, so disable the test + return + + # timesyncd has ConditionVirtualization=!container by default; drop/mock that for testing + if systemd-detect-virt --container --quiet; then + systemctl disable --quiet --now systemd-timesyncd + mkdir -p /run/systemd/system/systemd-timesyncd.service.d + cat >/run/systemd/system/systemd-timesyncd.service.d/container.conf <<EOF +[Unit] +ConditionVirtualization= + +[Service] +Type=simple +AmbientCapabilities= +ExecStart= +ExecStart=/bin/sleep infinity +EOF + systemctl daemon-reload + fi + + systemd-run --unit busctl-monitor.service --service-type=notify \ + busctl monitor --json=short --match="type=signal,sender=org.freedesktop.timedate1,member=PropertiesChanged,path=/org/freedesktop/timedate1" + + : 'Disable NTP' + ts="$(date +"%F %T.%6N")" + timedatectl set-ntp false + assert_timedated_signal "$ts" "false" + assert_timesyncd_state "inactive" + assert_ntp "false" + assert_rc 3 systemctl is-active --quiet systemd-timesyncd + + : 'Enable NTP' + ts="$(date +"%F %T.%6N")" + timedatectl set-ntp true + assert_timedated_signal "$ts" "true" + assert_ntp "true" + assert_timesyncd_state "active" + assert_rc 0 systemctl is-active --quiet systemd-timesyncd + + : 'Re-disable NTP' + ts="$(date +"%F %T.%6N")" + timedatectl set-ntp false + assert_timedated_signal "$ts" "false" + assert_ntp "false" + assert_rc 3 systemctl is-active --quiet systemd-timesyncd + + systemctl stop busctl-monitor.service + rm -rf /run/systemd/system/systemd-timesyncd.service.d/ + systemctl daemon-reload +} + +assert_timesyncd_signal() { + local timestamp="${1:?}" + local property="${2:?}" + local value="${3:?}" + local args=(-q --since="$timestamp" -p info _SYSTEMD_UNIT="busctl-monitor.service") + + journalctl --sync + + for _ in {0..9}; do + if journalctl "${args[@]}" --grep .; then + [[ "$(journalctl "${args[@]}" -o cat | jq -r ".payload.data[1].$property.data | join(\" \")")" == "$value" ]]; + return 0 + fi + + sleep .5 + done + + return 1 +} + +assert_networkd_ntp() { + local interface="${1:?}" + local value="${2:?}" + # Go through the array of NTP servers and for each entry do: + # - if the entry is an IPv4 address, join the Address array into a dot separated string + # - if the entry is a server address, select it unchanged + # These steps produce an array of strings, that is then joined into a space-separated string + # Note: this doesn't support IPv6 addresses, since converting them to a string is a bit more + # involved than a simple join(), but let's leave that to another time + local expr='[.NTP[] | (select(.Family == 2).Address | join(".")), select(has("Server")).Server] | join(" ")' + + [[ "$(networkctl status "$interface" --json=short | jq -r "$expr")" == "$value" ]] +} + +testcase_timesyncd() { + if systemd-detect-virt -cq; then + echo "This test case requires a VM, skipping..." + return 0 + fi + + if ! command -v networkctl >/dev/null; then + echo "This test requires systemd-networkd, skipping..." + return 0 + fi + + # Create a dummy interface managed by networkd, so we can configure link NTP servers + mkdir -p /run/systemd/network/ + cat >/etc/systemd/network/10-ntp99.netdev <<EOF +[NetDev] +Name=ntp99 +Kind=dummy +EOF + cat >/etc/systemd/network/10-ntp99.network <<EOF +[Match] +Name=ntp99 + +[Network] +Address=10.0.0.1/24 +EOF + + systemctl unmask systemd-timesyncd systemd-networkd + systemctl restart systemd-timesyncd + systemctl restart systemd-networkd + networkctl status ntp99 + + systemd-run --unit busctl-monitor.service --service-type=notify \ + busctl monitor --json=short --match="type=signal,sender=org.freedesktop.timesync1,member=PropertiesChanged,path=/org/freedesktop/timesync1" + + # LinkNTPServers + # + # Single IP + ts="$(date +"%F %T.%6N")" + timedatectl ntp-servers ntp99 10.0.0.1 + assert_networkd_ntp ntp99 10.0.0.1 + assert_timesyncd_signal "$ts" LinkNTPServers 10.0.0.1 + # Setting NTP servers to the same value shouldn't emit a PropertiesChanged signal + ts="$(date +"%F %T.%6N")" + timedatectl ntp-servers ntp99 10.0.0.1 + assert_networkd_ntp ntp99 10.0.0.1 + (! assert_timesyncd_signal "$ts" LinkNTPServers 10.0.0.1) + # Multiple IPs + ts="$(date +"%F %T.%6N")" + timedatectl ntp-servers ntp99 10.0.0.1 192.168.0.99 + assert_networkd_ntp ntp99 "10.0.0.1 192.168.0.99" + assert_timesyncd_signal "$ts" LinkNTPServers "10.0.0.1 192.168.0.99" + # Multiple IPs + servers + ts="$(date +"%F %T.%6N")" + timedatectl ntp-servers ntp99 10.0.0.1 192.168.0.99 foo.localhost foo 10.11.12.13 + assert_networkd_ntp ntp99 "10.0.0.1 192.168.0.99 foo.localhost foo 10.11.12.13" + assert_timesyncd_signal "$ts" LinkNTPServers "10.0.0.1 192.168.0.99 foo.localhost foo 10.11.12.13" + + # RuntimeNTPServers + # + # There's no user-facing API that allows changing this property (afaik), so let's + # call SetRuntimeNTPServers() directly to test things out. The inner workings should + # be exactly the same as in the previous case, so do just one test to make sure + # things work + ts="$(date +"%F %T.%6N")" + busctl call org.freedesktop.timesync1 /org/freedesktop/timesync1 org.freedesktop.timesync1.Manager \ + SetRuntimeNTPServers as 4 "10.0.0.1" foo "192.168.99.1" bar + servers="$(busctl get-property org.freedesktop.timesync1 /org/freedesktop/timesync1 org.freedesktop.timesync1.Manager RuntimeNTPServers)" + [[ "$servers" == 'as 4 "10.0.0.1" "foo" "192.168.99.1" "bar"' ]] + assert_timesyncd_signal "$ts" RuntimeNTPServers "10.0.0.1 foo 192.168.99.1 bar" + + # Cleanup + systemctl stop systemd-networkd systemd-timesyncd + rm -f /run/systemd/network/ntp99.* +} + +run_testcases + +touch /testok diff --git a/test/units/testsuite-46.service b/test/units/testsuite-46.service new file mode 100644 index 0000000..5efb9cc --- /dev/null +++ b/test/units/testsuite-46.service @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-46-HOMED +Wants=getty-pre.target +Before=getty-pre.target +Requires=systemd-homed.service systemd-userdbd.socket +After=systemd-homed.service systemd-userdbd.socket + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot +NotifyAccess=all diff --git a/test/units/testsuite-46.sh b/test/units/testsuite-46.sh new file mode 100755 index 0000000..a77683b --- /dev/null +++ b/test/units/testsuite-46.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Check if homectl is installed, and if it isn't bail out early instead of failing +if ! test -x /usr/bin/homectl ; then + echo "no homed" >/skipped + exit 0 +fi + +inspect() { + # As updating disk-size-related attributes can take some time on some + # filesystems, let's drop these fields before comparing the outputs to + # avoid unexpected fails. To see the full outputs of both homectl & + # userdbctl (for debugging purposes) drop the fields just before the + # comparison. + local USERNAME="${1:?}" + homectl inspect "$USERNAME" | tee /tmp/a + userdbctl user "$USERNAME" | tee /tmp/b + + # diff uses the grep BREs for pattern matching + diff -I '^\s*Disk \(Size\|Free\|Floor\|Ceiling\):' /tmp/{a,b} + rm /tmp/{a,b} + + homectl inspect --json=pretty "$USERNAME" +} + +wait_for_state() { + for i in {1..10}; do + (( i > 1 )) && sleep 0.5 + homectl inspect "$1" | grep -qF "State: $2" && break + done +} + +systemd-analyze log-level debug +systemctl service-log-level systemd-homed debug + +# Create a tmpfs to use as backing store for the home dir. That way we can enforce a size limit nicely. +mkdir -p /home +mount -t tmpfs tmpfs /home -o size=290M + +# we enable --luks-discard= since we run our tests in a tight VM, hence don't +# needlessly pressure for storage. We also set the cheapest KDF, since we don't +# want to waste CI CPU cycles on it. +NEWPASSWORD=xEhErW0ndafV4s homectl create test-user \ + --disk-size=min \ + --luks-discard=yes \ + --image-path=/home/test-user.home \ + --luks-pbkdf-type=pbkdf2 \ + --luks-pbkdf-time-cost=1ms +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl authenticate test-user + +PASSWORD=xEhErW0ndafV4s homectl activate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl update test-user --real-name="Inline test" +inspect test-user + +homectl deactivate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s NEWPASSWORD=yPN4N0fYNKUkOq homectl passwd test-user +inspect test-user + +PASSWORD=yPN4N0fYNKUkOq homectl activate test-user +inspect test-user + +SYSTEMD_LOG_LEVEL=debug PASSWORD=yPN4N0fYNKUkOq NEWPASSWORD=xEhErW0ndafV4s homectl passwd test-user +inspect test-user + +homectl deactivate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl activate test-user +inspect test-user + +homectl deactivate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl update test-user --real-name="Offline test" +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl activate test-user +inspect test-user + +homectl deactivate test-user +inspect test-user + +# Do some resize tests, but only if we run on real kernels, as quota inside of containers will fail +if ! systemd-detect-virt -cq ; then + # grow while inactive + PASSWORD=xEhErW0ndafV4s homectl resize test-user 300M + inspect test-user + + # minimize while inactive + PASSWORD=xEhErW0ndafV4s homectl resize test-user min + inspect test-user + + PASSWORD=xEhErW0ndafV4s homectl activate test-user + inspect test-user + + # grow while active + PASSWORD=xEhErW0ndafV4s homectl resize test-user max + inspect test-user + + # minimize while active + PASSWORD=xEhErW0ndafV4s homectl resize test-user 0 + inspect test-user + + # grow while active + PASSWORD=xEhErW0ndafV4s homectl resize test-user 300M + inspect test-user + + # shrink to original size while active + PASSWORD=xEhErW0ndafV4s homectl resize test-user 256M + inspect test-user + + # minimize again + PASSWORD=xEhErW0ndafV4s homectl resize test-user min + inspect test-user + + # Increase space, so that we can reasonably rebalance free space between to home dirs + mount /home -o remount,size=800M + + # create second user + NEWPASSWORD=uuXoo8ei homectl create test-user2 \ + --disk-size=min \ + --luks-discard=yes \ + --image-path=/home/test-user2.home \ + --luks-pbkdf-type=pbkdf2 \ + --luks-pbkdf-time-cost=1ms + inspect test-user2 + + # activate second user + PASSWORD=uuXoo8ei homectl activate test-user2 + inspect test-user2 + + # set second user's rebalance weight to 100 + PASSWORD=uuXoo8ei homectl update test-user2 --rebalance-weight=100 + inspect test-user2 + + # set first user's rebalance weight to quarter of that of the second + PASSWORD=xEhErW0ndafV4s homectl update test-user --rebalance-weight=25 + inspect test-user + + # synchronously rebalance + homectl rebalance + inspect test-user + inspect test-user2 +fi + +PASSWORD=xEhErW0ndafV4s homectl with test-user -- test ! -f /home/test-user/xyz +(! PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz) +PASSWORD=xEhErW0ndafV4s homectl with test-user -- touch /home/test-user/xyz +PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz +PASSWORD=xEhErW0ndafV4s homectl with test-user -- rm /home/test-user/xyz +PASSWORD=xEhErW0ndafV4s homectl with test-user -- test ! -f /home/test-user/xyz +(! PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz) + +wait_for_state test-user inactive +homectl remove test-user + +if ! systemd-detect-virt -cq ; then + wait_for_state test-user2 active + homectl deactivate test-user2 + wait_for_state test-user2 inactive + homectl remove test-user2 +fi + +# userdbctl tests +export PAGER= + +# Create a couple of user/group records to test io.systemd.DropIn +# See docs/USER_RECORD.md and docs/GROUP_RECORD.md +mkdir -p /run/userdb/ +cat >"/run/userdb/dropingroup.group" <<\EOF +{ + "groupName" : "dropingroup", + "gid" : 1000000 +} +EOF +cat >"/run/userdb/dropinuser.user" <<\EOF +{ + "userName" : "dropinuser", + "uid" : 2000000, + "realName" : "🐱", + "memberOf" : [ + "dropingroup" + ] +} +EOF +cat >"/run/userdb/dropinuser.user-privileged" <<\EOF +{ + "privileged" : { + "hashedPassword" : [ + "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/" + ], + "sshAuthorizedKeys" : [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA//dxI2xLg4MgxIKKZv1nqwTEIlE/fdakii2Fb75pG+ foo@bar.tld", + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMlaqG2rTMje5CQnfjXJKmoSpEVJ2gWtx4jBvsQbmee2XbU/Qdq5+SRisssR9zVuxgg5NA5fv08MgjwJQMm+csc= hello@world.tld" + ] + } +} +EOF +# Set permissions and create necessary symlinks as described in nss-systemd(8) +chmod 0600 "/run/userdb/dropinuser.user-privileged" +ln -svrf "/run/userdb/dropingroup.group" "/run/userdb/1000000.group" +ln -svrf "/run/userdb/dropinuser.user" "/run/userdb/2000000.user" +ln -svrf "/run/userdb/dropinuser.user-privileged" "/run/userdb/2000000.user-privileged" + +userdbctl +userdbctl --version +userdbctl --help --no-pager +userdbctl --no-legend +userdbctl --output=classic +userdbctl --output=friendly +userdbctl --output=table +userdbctl --output=json | jq +userdbctl -j --json=pretty | jq +userdbctl -j --json=short | jq +userdbctl --with-varlink=no + +userdbctl user +userdbctl user testuser +userdbctl user root +userdbctl user testuser root +userdbctl user -j testuser root | jq +# Check only UID for the nobody user, since the name is build-configurable +userdbctl user --with-nss=no --synthesize=yes +userdbctl user --with-nss=no --synthesize=yes 0 root 65534 +userdbctl user dropinuser +userdbctl user 2000000 +userdbctl user --with-nss=no --with-varlink=no --synthesize=no --multiplexer=no dropinuser +userdbctl user --with-nss=no 2000000 +(! userdbctl user '') +(! userdbctl user 🐱) +(! userdbctl user 🐱 '' bar) +(! userdbctl user i-do-not-exist) +(! userdbctl user root i-do-not-exist testuser) +(! userdbctl user --with-nss=no --synthesize=no 0 root 65534) +(! userdbctl user -N root nobody) +(! userdbctl user --with-dropin=no dropinuser) +(! userdbctl user --with-dropin=no 2000000) + +userdbctl group +userdbctl group testuser +userdbctl group root +userdbctl group testuser root +userdbctl group -j testuser root | jq +# Check only GID for the nobody group, since the name is build-configurable +userdbctl group --with-nss=no --synthesize=yes +userdbctl group --with-nss=no --synthesize=yes 0 root 65534 +userdbctl group dropingroup +userdbctl group 1000000 +userdbctl group --with-nss=no --with-varlink=no --synthesize=no --multiplexer=no dropingroup +userdbctl group --with-nss=no 1000000 +(! userdbctl group '') +(! userdbctl group 🐱) +(! userdbctl group 🐱 '' bar) +(! userdbctl group i-do-not-exist) +(! userdbctl group root i-do-not-exist testuser) +(! userdbctl group --with-nss=no --synthesize=no 0 root 65534) +(! userdbctl group --with-dropin=no dropingroup) +(! userdbctl group --with-dropin=no 1000000) + +userdbctl users-in-group +userdbctl users-in-group testuser +userdbctl users-in-group testuser root +userdbctl users-in-group -j testuser root | jq +userdbctl users-in-group 🐱 +(! userdbctl users-in-group '') +(! userdbctl users-in-group foo '' bar) + +userdbctl groups-of-user +userdbctl groups-of-user testuser +userdbctl groups-of-user testuser root +userdbctl groups-of-user -j testuser root | jq +userdbctl groups-of-user 🐱 +(! userdbctl groups-of-user '') +(! userdbctl groups-of-user foo '' bar) + +userdbctl services +userdbctl services -j | jq + +varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"userName":"testuser","service":"io.systemd.Multiplexer"}' +varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"userName":"root","service":"io.systemd.Multiplexer"}' +varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"userName":"dropinuser","service":"io.systemd.Multiplexer"}' +varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"uid":2000000,"service":"io.systemd.Multiplexer"}' +(! varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"userName":"","service":"io.systemd.Multiplexer"}') +(! varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"userName":"🐱","service":"io.systemd.Multiplexer"}') +(! varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{"userName":"i-do-not-exist","service":"io.systemd.Multiplexer"}') + +userdbctl ssh-authorized-keys dropinuser | tee /tmp/authorized-keys +grep "ssh-ed25519" /tmp/authorized-keys +grep "ecdsa-sha2-nistp256" /tmp/authorized-keys +echo "my-top-secret-key 🐱" >/tmp/my-top-secret-key +userdbctl ssh-authorized-keys dropinuser --chain /bin/cat /tmp/my-top-secret-key | tee /tmp/authorized-keys +grep "ssh-ed25519" /tmp/authorized-keys +grep "ecdsa-sha2-nistp256" /tmp/authorized-keys +grep "my-top-secret-key 🐱" /tmp/authorized-keys +(! userdbctl ssh-authorized-keys 🐱) +(! userdbctl ssh-authorized-keys dropin-user --chain) +(! userdbctl ssh-authorized-keys dropin-user --chain '') +(! SYSTEMD_LOG_LEVEL=debug userdbctl ssh-authorized-keys dropin-user --chain /bin/false) + +(! userdbctl '') +for opt in json multiplexer output synthesize with-dropin with-nss with-varlink; do + (! userdbctl "--$opt=''") + (! userdbctl "--$opt='🐱'") + (! userdbctl "--$opt=foo") + (! userdbctl "--$opt=foo" "--$opt=''" "--$opt=🐱") +done + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-50.service b/test/units/testsuite-50.service new file mode 100644 index 0000000..bcafe6e --- /dev/null +++ b/test/units/testsuite-50.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-50-DISSECT + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh new file mode 100755 index 0000000..28218ab --- /dev/null +++ b/test/units/testsuite-50.sh @@ -0,0 +1,718 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug + +# shellcheck disable=SC2317 +cleanup() {( + set +ex + + if [ -z "${image_dir}" ]; then + return + fi + umount "${image_dir}/app0" + umount "${image_dir}/app1" + umount "${image_dir}/app-nodistro" + umount "${image_dir}/service-scoped-test" + rm -rf "${image_dir}" +)} + +udevadm control --log-level=debug + +cd /tmp + +image_dir="$(mktemp -d -t -p /tmp tmp.XXXXXX)" +if [ -z "${image_dir}" ] || [ ! -d "${image_dir}" ]; then + echo "mktemp under /tmp failed" + exit 1 +fi + +trap cleanup EXIT + +cp /usr/share/minimal* "${image_dir}/" +image="${image_dir}/minimal_0" +roothash="$(cat "${image}.roothash")" + +os_release="$(test -e /etc/os-release && echo /etc/os-release || echo /usr/lib/os-release)" + +systemd-dissect --json=short "${image}.raw" | grep -q -F '{"rw":"ro","designator":"root","partition_uuid":null,"partition_label":null,"fstype":"squashfs","architecture":null,"verity":"external"' +systemd-dissect "${image}.raw" | grep -q -F "MARKER=1" +systemd-dissect "${image}.raw" | grep -q -F -f <(sed 's/"//g' "$os_release") + +systemd-dissect --list "${image}.raw" | grep -q '^etc/os-release$' +systemd-dissect --mtree "${image}.raw" --mtree-hash yes | grep -qe "^./usr/bin/cat type=file mode=0755 uid=0 gid=0 size=[0-9]* sha256sum=[a-z0-9]*$" +systemd-dissect --mtree "${image}.raw" --mtree-hash no | grep -qe "^./usr/bin/cat type=file mode=0755 uid=0 gid=0 size=[0-9]*$" + +read -r SHA256SUM1 _ < <(systemd-dissect --copy-from "${image}.raw" etc/os-release | sha256sum) +test "$SHA256SUM1" != "" +read -r SHA256SUM2 _ < <(systemd-dissect --read-only --with "${image}.raw" sha256sum etc/os-release) +test "$SHA256SUM2" != "" +test "$SHA256SUM1" = "$SHA256SUM2" + +mv "${image}.verity" "${image}.fooverity" +mv "${image}.roothash" "${image}.foohash" +systemd-dissect --json=short "${image}.raw" --root-hash="${roothash}" --verity-data="${image}.fooverity" | grep -q -F '{"rw":"ro","designator":"root","partition_uuid":null,"partition_label":null,"fstype":"squashfs","architecture":null,"verity":"external"' +systemd-dissect "${image}.raw" --root-hash="${roothash}" --verity-data="${image}.fooverity" | grep -q -F "MARKER=1" +systemd-dissect "${image}.raw" --root-hash="${roothash}" --verity-data="${image}.fooverity" | grep -q -F -f <(sed 's/"//g' "$os_release") +mv "${image}.fooverity" "${image}.verity" +mv "${image}.foohash" "${image}.roothash" + +mkdir -p "${image_dir}/mount" "${image_dir}/mount2" +systemd-dissect --mount "${image}.raw" "${image_dir}/mount" +grep -q -F -f "$os_release" "${image_dir}/mount/usr/lib/os-release" +grep -q -F -f "$os_release" "${image_dir}/mount/etc/os-release" +grep -q -F "MARKER=1" "${image_dir}/mount/usr/lib/os-release" +# Verity volume should be shared (opened only once) +systemd-dissect --mount "${image}.raw" "${image_dir}/mount2" +verity_count=$(find /dev/mapper/ -name "*verity*" | wc -l) +# In theory we should check that count is exactly one. In practice, libdevmapper +# randomly and unpredictably fails with an unhelpful EINVAL when a device is open +# (and even mounted and in use), so best-effort is the most we can do for now +if [ "${verity_count}" -lt 1 ]; then + echo "Verity device ${image}.raw not found in /dev/mapper/" + exit 1 +fi +systemd-dissect --umount "${image_dir}/mount" +systemd-dissect --umount "${image_dir}/mount2" + +systemd-run -P -p RootImage="${image}.raw" cat /usr/lib/os-release | grep -q -F "MARKER=1" +mv "${image}.verity" "${image}.fooverity" +mv "${image}.roothash" "${image}.foohash" +systemd-run -P -p RootImage="${image}.raw" -p RootHash="${image}.foohash" -p RootVerity="${image}.fooverity" cat /usr/lib/os-release | grep -q -F "MARKER=1" +# Let's use the long option name just here as a test +systemd-run -P --property RootImage="${image}.raw" --property RootHash="${roothash}" --property RootVerity="${image}.fooverity" cat /usr/lib/os-release | grep -q -F "MARKER=1" +mv "${image}.fooverity" "${image}.verity" +mv "${image}.foohash" "${image}.roothash" + +# Make a GPT disk on the fly, with the squashfs as partition 1 and the verity hash tree as partition 2 +machine="$(uname -m)" +if [ "${machine}" = "x86_64" ]; then + root_guid=4f68bce3-e8cd-4db1-96e7-fbcaf984b709 + verity_guid=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5 + signature_guid=41092b05-9fc8-4523-994f-2def0408b176 + architecture="x86-64" +elif [ "${machine}" = "i386" ] || [ "${machine}" = "i686" ] || [ "${machine}" = "x86" ]; then + root_guid=44479540-f297-41b2-9af7-d131d5f0458a + verity_guid=d13c5d3b-b5d1-422a-b29f-9454fdc89d76 + signature_guid=5996fc05-109c-48de-808b-23fa0830b676 + architecture="x86" +elif [ "${machine}" = "aarch64" ] || [ "${machine}" = "aarch64_be" ] || [ "${machine}" = "armv8b" ] || [ "${machine}" = "armv8l" ]; then + root_guid=b921b045-1df0-41c3-af44-4c6f280d3fae + verity_guid=df3300ce-d69f-4c92-978c-9bfb0f38d820 + signature_guid=6db69de6-29f4-4758-a7a5-962190f00ce3 + architecture="arm64" +elif [ "${machine}" = "arm" ]; then + root_guid=69dad710-2ce4-4e3c-b16c-21a1d49abed3 + verity_guid=7386cdf2-203c-47a9-a498-f2ecce45a2d6 + signature_guid=42b0455f-eb11-491d-98d3-56145ba9d037 + architecture="arm" +elif [ "${machine}" = "loongarch64" ]; then + root_guid=77055800-792c-4f94-b39a-98c91b762bb6 + verity_guid=f3393b22-e9af-4613-a948-9d3bfbd0c535 + signature_guid=5afb67eb-ecc8-4f85-ae8e-ac1e7c50e7d0 + architecture="loongarch64" +elif [ "${machine}" = "ia64" ]; then + root_guid=993d8d3d-f80e-4225-855a-9daf8ed7ea97 + verity_guid=86ed10d5-b607-45bb-8957-d350f23d0571 + signature_guid=e98b36ee-32ba-4882-9b12-0ce14655f46a + architecture="ia64" +elif [ "${machine}" = "s390x" ]; then + root_guid=5eead9a9-fe09-4a1e-a1d7-520d00531306 + verity_guid=b325bfbe-c7be-4ab8-8357-139e652d2f6b + signature_guid=c80187a5-73a3-491a-901a-017c3fa953e9 + architecture="s390x" +elif [ "${machine}" = "ppc64le" ]; then + root_guid=c31c45e6-3f39-412e-80fb-4809c4980599 + verity_guid=906bd944-4589-4aae-a4e4-dd983917446a + signature_guid=d4a236e7-e873-4c07-bf1d-bf6cf7f1c3c6 + architecture="ppc64-le" +else + echo "Unexpected uname -m: ${machine} in testsuite-50.sh, please fix me" + exit 1 +fi +# du rounds up to block size, which is more helpful for partitioning +root_size="$(du -k "${image}.raw" | cut -f1)" +verity_size="$(du -k "${image}.verity" | cut -f1)" +signature_size=4 +# 4MB seems to be the minimum size blkid will accept, below that probing fails +dd if=/dev/zero of="${image}.gpt" bs=512 count=$((8192+root_size*2+verity_size*2+signature_size*2)) +# sfdisk seems unhappy if the size overflows into the next unit, eg: 1580KiB will be interpreted as 1MiB +# so do some basic rounding up if the minimal image is more than 1 MB +if [ "${root_size}" -ge 1024 ]; then + root_size="$((root_size/1024 + 1))MiB" +else + root_size="${root_size}KiB" +fi +verity_size="$((verity_size * 2))KiB" +signature_size="$((signature_size * 2))KiB" + +HAVE_OPENSSL=0 +if systemctl --version | grep -q -- +OPENSSL ; then + # The openssl binary is installed conditionally. + # If we have OpenSSL support enabled and openssl is missing, fail early + # with a proper error message. + if ! command -v openssl >/dev/null 2>&1; then + echo "openssl missing" >/failed + exit 1 + fi + + HAVE_OPENSSL=1 + OPENSSL_CONFIG="$(mktemp)" + # Unfortunately OpenSSL insists on reading some config file, hence provide one with mostly placeholder contents + cat >"${OPENSSL_CONFIG:?}" <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = DE +ST = Test State +L = Test Locality +O = Org Name +OU = Org Unit Name +CN = Common Name +emailAddress = test@email.com +EOF + + # Create key pair + openssl req -config "$OPENSSL_CONFIG" -new -x509 -newkey rsa:1024 -keyout "${image}.key" -out "${image}.crt" -days 365 -nodes + # Sign Verity root hash with it + openssl smime -sign -nocerts -noattr -binary -in "${image}.roothash" -inkey "${image}.key" -signer "${image}.crt" -outform der -out "${image}.roothash.p7s" + # Generate signature partition JSON data + echo '{"rootHash":"'"${roothash}"'","signature":"'"$(base64 -w 0 <"${image}.roothash.p7s")"'"}' >"${image}.verity-sig" + # Pad it + truncate -s "${signature_size}" "${image}.verity-sig" + # Register certificate in the (userspace) verity key ring + mkdir -p /run/verity.d + ln -s "${image}.crt" /run/verity.d/ok.crt +fi + +# Construct a UUID from hash +# input: 11111111222233334444555566667777 +# output: 11111111-2222-3333-4444-555566667777 +uuid="$(head -c 32 "${image}.roothash" | sed -r 's/(.{8})(.{4})(.{4})(.{4})(.+)/\1-\2-\3-\4-\5/')" +echo -e "label: gpt\nsize=${root_size}, type=${root_guid}, uuid=${uuid}" | sfdisk "${image}.gpt" +uuid="$(tail -c 32 "${image}.roothash" | sed -r 's/(.{8})(.{4})(.{4})(.{4})(.+)/\1-\2-\3-\4-\5/')" +echo -e "size=${verity_size}, type=${verity_guid}, uuid=${uuid}" | sfdisk "${image}.gpt" --append +if [ "${HAVE_OPENSSL}" -eq 1 ]; then + echo -e "size=${signature_size}, type=${signature_guid}" | sfdisk "${image}.gpt" --append +fi +sfdisk --part-label "${image}.gpt" 1 "Root Partition" +sfdisk --part-label "${image}.gpt" 2 "Verity Partition" +if [ "${HAVE_OPENSSL}" -eq 1 ]; then + sfdisk --part-label "${image}.gpt" 3 "Signature Partition" +fi +loop="$(losetup --show -P -f "${image}.gpt")" +partitions=( + "${loop:?}p1" + "${loop:?}p2" +) +if [ "${HAVE_OPENSSL}" -eq 1 ]; then + partitions+=( "${loop:?}p3" ) +fi +# The kernel sometimes(?) does not emit "add" uevent for loop block partition devices. +# Let's not expect the devices to be initialized. +udevadm wait --timeout 60 --settle --initialized=no "${partitions[@]}" +udevadm lock --device="${loop}p1" dd if="${image}.raw" of="${loop}p1" +udevadm lock --device="${loop}p2" dd if="${image}.verity" of="${loop}p2" +if [ "${HAVE_OPENSSL}" -eq 1 ]; then + udevadm lock --device="${loop}p3" dd if="${image}.verity-sig" of="${loop}p3" +fi +losetup -d "${loop}" + +# Derive partition UUIDs from root hash, in UUID syntax +ROOT_UUID="$(systemd-id128 -u show "$(head -c 32 "${image}.roothash")" -u | tail -n 1 | cut -b 6-)" +VERITY_UUID="$(systemd-id128 -u show "$(tail -c 32 "${image}.roothash")" -u | tail -n 1 | cut -b 6-)" + +systemd-dissect --json=short --root-hash "${roothash}" "${image}.gpt" | grep -q '{"rw":"ro","designator":"root","partition_uuid":"'"$ROOT_UUID"'","partition_label":"Root Partition","fstype":"squashfs","architecture":"'"$architecture"'","verity":"signed",' +systemd-dissect --json=short --root-hash "${roothash}" "${image}.gpt" | grep -q '{"rw":"ro","designator":"root-verity","partition_uuid":"'"$VERITY_UUID"'","partition_label":"Verity Partition","fstype":"DM_verity_hash","architecture":"'"$architecture"'","verity":null,' +if [ "${HAVE_OPENSSL}" -eq 1 ]; then + systemd-dissect --json=short --root-hash "${roothash}" "${image}.gpt" | grep -q -E '{"rw":"ro","designator":"root-verity-sig","partition_uuid":"'".*"'","partition_label":"Signature Partition","fstype":"verity_hash_signature","architecture":"'"$architecture"'","verity":null,' +fi +systemd-dissect --root-hash "${roothash}" "${image}.gpt" | grep -q -F "MARKER=1" +systemd-dissect --root-hash "${roothash}" "${image}.gpt" | grep -q -F -f <(sed 's/"//g' "$os_release") + +# Test image policies +systemd-dissect --validate "${image}.gpt" +systemd-dissect --validate "${image}.gpt" --image-policy='*' +(! systemd-dissect --validate "${image}.gpt" --image-policy='~') +(! systemd-dissect --validate "${image}.gpt" --image-policy='-') +(! systemd-dissect --validate "${image}.gpt" --image-policy=root=absent) +(! systemd-dissect --validate "${image}.gpt" --image-policy=swap=unprotected+encrypted+verity) +systemd-dissect --validate "${image}.gpt" --image-policy=root=unprotected +systemd-dissect --validate "${image}.gpt" --image-policy=root=verity +systemd-dissect --validate "${image}.gpt" --image-policy=root=verity:root-verity-sig=unused+absent +systemd-dissect --validate "${image}.gpt" --image-policy=root=verity:swap=absent +systemd-dissect --validate "${image}.gpt" --image-policy=root=verity:swap=absent+unprotected +(! systemd-dissect --validate "${image}.gpt" --image-policy=root=verity:root-verity=unused+absent) +systemd-dissect --validate "${image}.gpt" --image-policy=root=signed +(! systemd-dissect --validate "${image}.gpt" --image-policy=root=signed:root-verity-sig=unused+absent) +(! systemd-dissect --validate "${image}.gpt" --image-policy=root=signed:root-verity=unused+absent) + +# Test RootImagePolicy= unit file setting +systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='*' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1" +(! systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='~' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1") +(! systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='-' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1") +(! systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='root=absent' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1") +systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='root=verity' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='root=signed' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1" +(! systemd-run --wait -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p RootImagePolicy='root=encrypted' -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1") + +systemd-dissect --root-hash "${roothash}" --mount "${image}.gpt" "${image_dir}/mount" +grep -q -F -f "$os_release" "${image_dir}/mount/usr/lib/os-release" +grep -q -F -f "$os_release" "${image_dir}/mount/etc/os-release" +grep -q -F "MARKER=1" "${image_dir}/mount/usr/lib/os-release" +systemd-dissect --umount "${image_dir}/mount" + +systemd-dissect --root-hash "${roothash}" --mount "${image}.gpt" --in-memory "${image_dir}/mount" +grep -q -F -f "$os_release" "${image_dir}/mount/usr/lib/os-release" +grep -q -F -f "$os_release" "${image_dir}/mount/etc/os-release" +grep -q -F "MARKER=1" "${image_dir}/mount/usr/lib/os-release" +systemd-dissect --umount "${image_dir}/mount" + +# add explicit -p MountAPIVFS=yes once to test the parser +systemd-run -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1" + +systemd-run -P -p RootImage="${image}.raw" -p RootImageOptions="root:nosuid,dev home:ro,dev ro,noatime" mount | grep -F "squashfs" | grep -q -F "nosuid" +systemd-run -P -p RootImage="${image}.gpt" -p RootImageOptions="root:ro,noatime root:ro,dev" mount | grep -F "squashfs" | grep -q -F "noatime" + +mkdir -p "${image_dir}/result" +cat >/run/systemd/system/testservice-50a.service <<EOF +[Service] +Type=oneshot +ExecStart=bash -c "mount >/run/result/a" +BindPaths=${image_dir}/result:/run/result +TemporaryFileSystem=/run +RootImage=${image}.raw +RootImageOptions=root:ro,noatime home:ro,dev relatime,dev +RootImageOptions=nosuid,dev +EOF +systemctl start testservice-50a.service +grep -F "squashfs" "${image_dir}/result/a" | grep -q -F "noatime" +grep -F "squashfs" "${image_dir}/result/a" | grep -q -F -v "nosuid" + +cat >/run/systemd/system/testservice-50b.service <<EOF +[Service] +Type=oneshot +ExecStart=bash -c "mount >/run/result/b" +BindPaths=${image_dir}/result:/run/result +TemporaryFileSystem=/run +RootImage=${image}.gpt +RootImageOptions=root:ro,noatime,nosuid home:ro,dev nosuid,dev +RootImageOptions=home:ro,dev nosuid,dev,%%foo +# this is the default, but let's specify once to test the parser +MountAPIVFS=yes +EOF +systemctl start testservice-50b.service +grep -F "squashfs" "${image_dir}/result/b" | grep -q -F "noatime" + +# Check that specifier escape is applied %%foo → %foo +busctl get-property org.freedesktop.systemd1 /org/freedesktop/systemd1/unit/testservice_2d50b_2eservice org.freedesktop.systemd1.Service RootImageOptions | grep -F "nosuid,dev,%foo" + +# Now do some checks with MountImages, both by itself, with options and in combination with RootImage, and as single FS or GPT image +systemd-run -P -p MountImages="${image}.gpt:/run/img1 ${image}.raw:/run/img2" cat /run/img1/usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run -P -p MountImages="${image}.gpt:/run/img1 ${image}.raw:/run/img2" cat /run/img2/usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run -P -p MountImages="${image}.gpt:/run/img1 ${image}.raw:/run/img2:nosuid,dev" mount | grep -F "squashfs" | grep -q -F "nosuid" +systemd-run -P -p MountImages="${image}.gpt:/run/img1:root:nosuid ${image}.raw:/run/img2:home:suid" mount | grep -F "squashfs" | grep -q -F "nosuid" +systemd-run -P -p MountImages="${image}.raw:/run/img2\:3" cat /run/img2:3/usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run -P -p MountImages="${image}.raw:/run/img2\:3:nosuid" mount | grep -F "squashfs" | grep -q -F "nosuid" +systemd-run -P -p TemporaryFileSystem=/run -p RootImage="${image}.raw" -p MountImages="${image}.gpt:/run/img1 ${image}.raw:/run/img2" cat /usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run -P -p TemporaryFileSystem=/run -p RootImage="${image}.raw" -p MountImages="${image}.gpt:/run/img1 ${image}.raw:/run/img2" cat /run/img1/usr/lib/os-release | grep -q -F "MARKER=1" +systemd-run -P -p TemporaryFileSystem=/run -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p MountImages="${image}.gpt:/run/img1 ${image}.raw:/run/img2" cat /run/img2/usr/lib/os-release | grep -q -F "MARKER=1" +cat >/run/systemd/system/testservice-50c.service <<EOF +[Service] +MountAPIVFS=yes +TemporaryFileSystem=/run +RootImage=${image}.raw +MountImages=${image}.gpt:/run/img1:root:noatime:home:relatime +MountImages=${image}.raw:/run/img2\:3:nosuid +ExecStart=bash -c "cat /run/img1/usr/lib/os-release >/run/result/c" +ExecStart=bash -c "cat /run/img2:3/usr/lib/os-release >>/run/result/c" +ExecStart=bash -c "mount >>/run/result/c" +BindPaths=${image_dir}/result:/run/result +Type=oneshot +EOF +systemctl start testservice-50c.service +grep -q -F "MARKER=1" "${image_dir}/result/c" +grep -F "squashfs" "${image_dir}/result/c" | grep -q -F "noatime" +grep -F "squashfs" "${image_dir}/result/c" | grep -q -F -v "nosuid" + +# Adding a new mounts at runtime works if the unit is in the active state, +# so use Type=notify to make sure there's no race condition in the test +cat >/run/systemd/system/testservice-50d.service <<EOF +[Service] +RuntimeMaxSec=300 +Type=notify +RemainAfterExit=yes +MountAPIVFS=yes +PrivateTmp=yes +ExecStart=/bin/sh -c ' \\ + systemd-notify --ready; \\ + while [ ! -f /tmp/img/usr/lib/os-release ] || ! grep -q -F MARKER /tmp/img/usr/lib/os-release; do \\ + sleep 0.1; \\ + done; \\ + mount; \\ + mount | grep -F "on /tmp/img type squashfs" | grep -q -F "nosuid"; \\ +' +EOF +systemctl start testservice-50d.service + +# Mount twice to exercise mount-beneath (on kernel 6.5+, on older kernels it will just overmount) +mkdir -p /tmp/wrong/foo +mksquashfs /tmp/wrong/foo /tmp/wrong.raw +systemctl mount-image --mkdir testservice-50d.service /tmp/wrong.raw /tmp/img +test "$(systemctl show -P SubState testservice-50d.service)" = "running" +systemctl mount-image --mkdir testservice-50d.service "${image}.raw" /tmp/img root:nosuid + +while systemctl show -P SubState testservice-50d.service | grep -q running +do + sleep 0.1 +done + +systemctl is-active testservice-50d.service + +# ExtensionImages will set up an overlay +systemd-run -P --property ExtensionImages=/usr/share/app0.raw --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0" +systemd-run -P --property ExtensionImages=/usr/share/app0.raw --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0" +systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /opt/script1.sh | grep -q -F "extension-release.app2" +systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/other_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionImages=/usr/share/app-nodistro.raw --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionImages=/etc/service-scoped-test.raw --property RootImage="${image}.raw" cat /etc/systemd/system/some_file | grep -q -F "MARKER_CONFEXT_123" +# Check that using a symlink to NAME-VERSION.raw works as long as the symlink has the correct name NAME.raw +mkdir -p /usr/share/symlink-test/ +cp /usr/share/app-nodistro.raw /usr/share/symlink-test/app-nodistro-v1.raw +ln -fs /usr/share/symlink-test/app-nodistro-v1.raw /usr/share/symlink-test/app-nodistro.raw +systemd-run -P --property ExtensionImages=/usr/share/symlink-test/app-nodistro.raw --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" + +# Symlink check again but for confext +mkdir -p /etc/symlink-test/ +cp /etc/service-scoped-test.raw /etc/symlink-test/service-scoped-test-v1.raw +ln -fs /etc/symlink-test/service-scoped-test-v1.raw /etc/symlink-test/service-scoped-test.raw +systemd-run -P --property ExtensionImages=/etc/symlink-test/service-scoped-test.raw --property RootImage="${image}.raw" cat /etc/systemd/system/some_file | grep -q -F "MARKER_CONFEXT_123" +# And again mixing sysext and confext +systemd-run -P \ + --property ExtensionImages=/usr/share/symlink-test/app-nodistro.raw \ + --property ExtensionImages=/etc/symlink-test/service-scoped-test.raw \ + --property RootImage="${image}.raw" cat /etc/systemd/system/some_file | grep -q -F "MARKER_CONFEXT_123" +systemd-run -P \ + --property ExtensionImages=/usr/share/symlink-test/app-nodistro.raw \ + --property ExtensionImages=/etc/symlink-test/service-scoped-test.raw \ + --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" + +cat >/run/systemd/system/testservice-50e.service <<EOF +[Service] +MountAPIVFS=yes +TemporaryFileSystem=/run /var/lib +StateDirectory=app0 +RootImage=${image}.raw +ExtensionImages=/usr/share/app0.raw /usr/share/app1.raw:nosuid +# Relevant only for sanitizer runs +UnsetEnvironment=LD_PRELOAD +ExecStart=/bin/bash -c '/opt/script0.sh | grep ID' +ExecStart=/bin/bash -c '/opt/script1.sh | grep ID' +Type=oneshot +RemainAfterExit=yes +EOF +systemctl start testservice-50e.service +systemctl is-active testservice-50e.service + +# ExtensionDirectories will set up an overlay +mkdir -p "${image_dir}/app0" "${image_dir}/app1" "${image_dir}/app-nodistro" "${image_dir}/service-scoped-test" +(! systemd-run -P --property ExtensionDirectories="${image_dir}/nonexistent" --property RootImage="${image}.raw" cat /opt/script0.sh) +(! systemd-run -P --property ExtensionDirectories="${image_dir}/app0" --property RootImage="${image}.raw" cat /opt/script0.sh) +systemd-dissect --mount /usr/share/app0.raw "${image_dir}/app0" +systemd-dissect --mount /usr/share/app1.raw "${image_dir}/app1" +systemd-dissect --mount /usr/share/app-nodistro.raw "${image_dir}/app-nodistro" +systemd-dissect --mount /etc/service-scoped-test.raw "${image_dir}/service-scoped-test" +systemd-run -P --property ExtensionDirectories="${image_dir}/app0" --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0" +systemd-run -P --property ExtensionDirectories="${image_dir}/app0" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0" +systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /opt/script1.sh | grep -q -F "extension-release.app2" +systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/other_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionDirectories="${image_dir}/app-nodistro" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1" +systemd-run -P --property ExtensionDirectories="${image_dir}/service-scoped-test" --property RootImage="${image}.raw" cat /etc/systemd/system/some_file | grep -q -F "MARKER_CONFEXT_123" +cat >/run/systemd/system/testservice-50f.service <<EOF +[Service] +MountAPIVFS=yes +TemporaryFileSystem=/run /var/lib +StateDirectory=app0 +RootImage=${image}.raw +ExtensionDirectories=${image_dir}/app0 ${image_dir}/app1 +# Relevant only for sanitizer runs +UnsetEnvironment=LD_PRELOAD +ExecStart=/bin/bash -c '/opt/script0.sh | grep ID' +ExecStart=/bin/bash -c '/opt/script1.sh | grep ID' +Type=oneshot +RemainAfterExit=yes +EOF +systemctl start testservice-50f.service +systemctl is-active testservice-50f.service +systemd-dissect --umount "${image_dir}/app0" +systemd-dissect --umount "${image_dir}/app1" + +# Test that an extension consisting of an empty directory under /etc/extensions/ takes precedence +mkdir -p /var/lib/extensions/ +ln -s /usr/share/app-nodistro.raw /var/lib/extensions/app-nodistro.raw +systemd-sysext merge +grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file +systemd-sysext unmerge +mkdir -p /etc/extensions/app-nodistro +systemd-sysext merge +test ! -e /usr/lib/systemd/system/some_file +systemd-sysext unmerge +rmdir /etc/extensions/app-nodistro + +# Similar, but go via varlink +varlinkctl call /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 +varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Refresh '{}' +grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file +varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Unmerge '{}' +(! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file ) + +# Check that extensions cannot contain os-release +mkdir -p /run/extensions/app-reject/usr/lib/{extension-release.d/,systemd/system} +echo "ID=_any" >/run/extensions/app-reject/usr/lib/extension-release.d/extension-release.app-reject +echo "ID=_any" >/run/extensions/app-reject/usr/lib/os-release +touch /run/extensions/app-reject/usr/lib/systemd/system/other_file +(! systemd-sysext merge) +test ! -e /usr/lib/systemd/system/some_file +test ! -e /usr/lib/systemd/system/other_file +systemd-sysext unmerge +rm -rf /run/extensions/app-reject +rm /var/lib/extensions/app-nodistro.raw + +mkdir -p /run/machines /run/portables /run/extensions +touch /run/machines/a.raw /run/portables/b.raw /run/extensions/c.raw + +systemd-dissect --discover --json=short >/tmp/discover.json +grep -q -F '{"name":"a","type":"raw","class":"machine","ro":false,"path":"/run/machines/a.raw"' /tmp/discover.json +grep -q -F '{"name":"b","type":"raw","class":"portable","ro":false,"path":"/run/portables/b.raw"' /tmp/discover.json +grep -q -F '{"name":"c","type":"raw","class":"sysext","ro":false,"path":"/run/extensions/c.raw"' /tmp/discover.json +rm /tmp/discover.json /run/machines/a.raw /run/portables/b.raw /run/extensions/c.raw + +# Check that the /sbin/mount.ddi helper works +T="/tmp/mounthelper.$RANDOM" +mount -t ddi "${image}.gpt" "$T" -o ro,X-mount.mkdir,discard +umount -R "$T" +rmdir "$T" + +LOOP="$(systemd-dissect --attach --loop-ref=waldo "${image}.raw")" + +# Wait until the symlinks we want to test are established +udevadm trigger -w "$LOOP" + +# Check if the /dev/loop/* symlinks really reference the right device +test /dev/disk/by-loop-ref/waldo -ef "$LOOP" + +if [ "$(stat -c '%Hd:%Ld' "${image}.raw")" != '?d:?d' ] ; then + # Old stat didn't know the %Hd and %Ld specifiers and turned them into ?d + # instead. Let's simply skip the test on such old systems. + test "$(stat -c '/dev/disk/by-loop-inode/%Hd:%Ld-%i' "${image}.raw")" -ef "$LOOP" +fi + +# Detach by loopback device +systemd-dissect --detach "$LOOP" + +# Test long reference name. +# Note, sizeof_field(struct loop_info64, lo_file_name) == 64, +# and --loop-ref accepts upto 63 characters, and udev creates symlink +# based on the name when it has upto _62_ characters. +name="$(for _ in {1..62}; do echo -n 'x'; done)" +LOOP="$(systemd-dissect --attach --loop-ref="$name" "${image}.raw")" +udevadm trigger -w "$LOOP" + +# Check if the /dev/disk/by-loop-ref/$name symlink really references the right device +test "/dev/disk/by-loop-ref/$name" -ef "$LOOP" + +# Detach by the /dev/disk/by-loop-ref symlink +systemd-dissect --detach "/dev/disk/by-loop-ref/$name" + +name="$(for _ in {1..63}; do echo -n 'x'; done)" +LOOP="$(systemd-dissect --attach --loop-ref="$name" "${image}.raw")" +udevadm trigger -w "$LOOP" + +# Check if the /dev/disk/by-loop-ref/$name symlink does not exist +test ! -e "/dev/disk/by-loop-ref/$name" + +# Detach by backing inode +systemd-dissect --detach "${image}.raw" +(! systemd-dissect --detach "${image}.raw") + +# check for confext functionality +mkdir -p /run/confexts/test/etc/extension-release.d +echo "ID=_any" >/run/confexts/test/etc/extension-release.d/extension-release.test +echo "ARCHITECTURE=_any" >>/run/confexts/test/etc/extension-release.d/extension-release.test +echo "MARKER_CONFEXT_123" >/run/confexts/test/etc/testfile +cat <<EOF >/run/confexts/test/etc/testscript +#!/bin/bash +echo "This should not happen" +EOF +chmod +x /run/confexts/test/etc/testscript +systemd-confext merge +grep -q -F "MARKER_CONFEXT_123" /etc/testfile +(! /etc/testscript) +systemd-confext status +systemd-confext unmerge +rm -rf /run/confexts/ + +unsquashfs -no-xattrs -d /tmp/img "${image}.raw" +systemd-run --unit=test-root-ephemeral \ + -p RootDirectory=/tmp/img \ + -p RootEphemeral=yes \ + -p Type=exec \ + bash -c "touch /abc && sleep infinity" +test -n "$(ls -A /var/lib/systemd/ephemeral-trees)" +systemctl stop test-root-ephemeral +# shellcheck disable=SC2016 +timeout 10 bash -c 'until test -z "$(ls -A /var/lib/systemd/ephemeral-trees)"; do sleep .5; done' +test ! -f /tmp/img/abc + +systemd-dissect --mtree /tmp/img +systemd-dissect --list /tmp/img + +read -r SHA256SUM1 _ < <(systemd-dissect --copy-from /tmp/img etc/os-release | sha256sum) +test "$SHA256SUM1" != "" + +echo abc > abc +systemd-dissect --copy-to /tmp/img abc /abc +test -f /tmp/img/abc + +# Test for dissect tool support with systemd-sysext +mkdir -p /run/extensions/ testkit/usr/lib/extension-release.d/ +echo "ID=_any" >testkit/usr/lib/extension-release.d/extension-release.testkit +echo "ARCHITECTURE=_any" >>testkit/usr/lib/extension-release.d/extension-release.testkit +echo "MARKER_SYSEXT_123" >testkit/usr/lib/testfile +mksquashfs testkit/ testkit.raw +cp testkit.raw /run/extensions/ +unsquashfs -l /run/extensions/testkit.raw +systemd-dissect --no-pager /run/extensions/testkit.raw | grep -q '✓ sysext for portable service' +systemd-dissect --no-pager /run/extensions/testkit.raw | grep -q '✓ sysext for system' +systemd-sysext merge +systemd-sysext status +grep -q -F "MARKER_SYSEXT_123" /usr/lib/testfile +systemd-sysext unmerge +rm -rf /run/extensions/ testkit/ + +# Test for dissect tool support with systemd-confext +mkdir -p /run/confexts/ testjob/etc/extension-release.d/ +echo "ID=_any" >testjob/etc/extension-release.d/extension-release.testjob +echo "ARCHITECTURE=_any" >>testjob/etc/extension-release.d/extension-release.testjob +echo "MARKER_CONFEXT_123" >testjob/etc/testfile +mksquashfs testjob/ testjob.raw +cp testjob.raw /run/confexts/ +unsquashfs -l /run/confexts/testjob.raw +systemd-dissect --no-pager /run/confexts/testjob.raw | grep -q '✓ confext for system' +systemd-dissect --no-pager /run/confexts/testjob.raw | grep -q '✓ confext for portable service' +systemd-confext merge +systemd-confext status +grep -q -F "MARKER_CONFEXT_123" /etc/testfile +systemd-confext unmerge +rm -rf /run/confexts/ testjob/ + +systemd-run -P -p RootImage="${image}.raw" cat /run/host/os-release | cmp "${os_release}" + +# Test that systemd-sysext reloads the daemon. +mkdir -p /var/lib/extensions/ +ln -s /usr/share/app-reload.raw /var/lib/extensions/app-reload.raw +systemd-sysext merge --no-reload +# the service should not be running +if systemctl --quiet is-active foo.service; then + echo "foo.service should not be active" + exit 1 +fi +systemd-sysext unmerge --no-reload +systemd-sysext merge +for RETRY in $(seq 60) LAST; do + if journalctl --boot --unit foo.service | grep -q -P 'echo\[[0-9]+\]: foo'; then + break + fi + if [ "${RETRY}" = LAST ]; then + echo "Output of foo.service not found" + exit 1 + fi + sleep 0.5 +done +systemd-sysext unmerge --no-reload +# Grep on the Warning to find the warning helper mentioning the daemon reload. +systemctl status foo.service 2>&1 | grep -q -F "Warning" +systemd-sysext merge +systemd-sysext unmerge +systemctl status foo.service 2>&1 | grep -v -q -F "Warning" +rm /var/lib/extensions/app-reload.raw + +# Test systemd-repart --make-ddi=: +if command -v mksquashfs >/dev/null 2>&1; then + + openssl req -config "$OPENSSL_CONFIG" -subj="/CN=waldo" -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout /tmp/test-50-privkey.key -out /tmp/test-50-cert.crt + + mkdir -p /tmp/test-50-confext/etc/extension-release.d/ + + echo "foobar50" > /tmp/test-50-confext/etc/waldo + + ( grep -e '^\(ID\|VERSION_ID\)=' /etc/os-release ; echo IMAGE_ID=waldo ; echo IMAGE_VERSION=7 ) > /tmp/test-50-confext/etc/extension-release.d/extension-release.waldo + + mkdir -p /run/confexts + + SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs systemd-repart -C -s /tmp/test-50-confext --certificate=/tmp/test-50-cert.crt --private-key=/tmp/test-50-privkey.key /run/confexts/waldo.confext.raw + rm -rf /tmp/test-50-confext + + mkdir -p /run/verity.d + cp /tmp/test-50-cert.crt /run/verity.d/ + systemd-dissect --mtree /run/confexts/waldo.confext.raw + + systemd-confext refresh + + read -r X < /etc/waldo + test "$X" = foobar50 + + rm /run/confexts/waldo.confext.raw + + systemd-confext refresh + + (! test -f /etc/waldo ) + + mkdir -p /tmp/test-50-sysext/usr/lib/extension-release.d/ + + # Make sure the sysext is big enough to not fit in the minimum partition size of repart so we know the + # Minimize= logic is working. + truncate --size=50M /tmp/test-50-sysext/usr/waldo + + ( grep -e '^\(ID\|VERSION_ID\)=' /etc/os-release ; echo IMAGE_ID=waldo ; echo IMAGE_VERSION=7 ) > /tmp/test-50-sysext/usr/lib/extension-release.d/extension-release.waldo + + mkdir -p /run/extensions + + SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs systemd-repart -S -s /tmp/test-50-sysext --certificate=/tmp/test-50-cert.crt --private-key=/tmp/test-50-privkey.key /run/extensions/waldo.sysext.raw + + systemd-dissect --mtree /run/extensions/waldo.sysext.raw + + systemd-sysext refresh + + test -f /usr/waldo + + rm /run/verity.d/test-50-cert.crt /run/extensions/waldo.sysext.raw /tmp/test-50-cert.crt /tmp/test-50-privkey.key + + systemd-sysext refresh + + (! test -f /usr/waldo) +fi + +# Sneak in a couple of expected-to-fail invocations to cover +# https://github.com/systemd/systemd/issues/29610 +(! systemd-run -P -p MountImages="/this/should/definitely/not/exist.img:/run/img2\:3:nosuid" false) +(! systemd-run -P -p ExtensionImages="/this/should/definitely/not/exist.img" false) +(! systemd-run -P -p RootImage="/this/should/definitely/not/exist.img" false) +(! systemd-run -P -p ExtensionDirectories="/foo/bar /foo/baz" false) + +touch /testok diff --git a/test/units/testsuite-52.service b/test/units/testsuite-52.service new file mode 100644 index 0000000..b9f2909 --- /dev/null +++ b/test/units/testsuite-52.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Testsuite service + +[Service] +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-52.sh b/test/units/testsuite-52.sh new file mode 100755 index 0000000..16ff507 --- /dev/null +++ b/test/units/testsuite-52.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +systemd-analyze log-level debug + +systemctl enable test-honor-first-shutdown.service +systemctl start test-honor-first-shutdown.service + +touch /testok diff --git a/test/units/testsuite-53.service b/test/units/testsuite-53.service new file mode 100644 index 0000000..cf3adbb --- /dev/null +++ b/test/units/testsuite-53.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-53-ISSUE-16347 + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-53.sh b/test/units/testsuite-53.sh new file mode 100755 index 0000000..84cd661 --- /dev/null +++ b/test/units/testsuite-53.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +: >/failed + +# Reset host date to current time, 3 days in the past. +date -s "-3 days" + +# Run a timer for every 15 minutes. +systemd-run --unit test-timer --on-calendar "*:0/15:0" true + +next_elapsed=$(systemctl show test-timer.timer -p NextElapseUSecRealtime --value) +next_elapsed=$(date -d "${next_elapsed}" +%s) +now=$(date +%s) +time_delta=$((next_elapsed - now)) + +# Check that the timer will elapse in less than 20 minutes. +((0 < time_delta && time_delta < 1200)) || { + echo 'Timer elapse outside of the expected 20 minute window.' + echo " next_elapsed=${next_elapsed}" + echo " now=${now}" + echo " time_delta=${time_delta}" + echo '' +} >>/failed + +if test ! -s /failed ; then + rm -f /failed + touch /testok +fi diff --git a/test/units/testsuite-54.service b/test/units/testsuite-54.service new file mode 100644 index 0000000..ba8cdad --- /dev/null +++ b/test/units/testsuite-54.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TESTSUITE-54-CREDS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh new file mode 100755 index 0000000..bcbe7a1 --- /dev/null +++ b/test/units/testsuite-54.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux + +systemd-analyze log-level debug + +run_with_cred_compare() { + local cred="${1:?}" + local exp="${2?}" + shift 2 + + diff <(systemd-run -p SetCredential="$cred" --wait --pipe -- systemd-creds "$@") <(echo -ne "$exp") +} + +# Sanity checks +# +# Create a dummy "full" disk (similar to /dev/full) to check out-of-space +# scenarios +mkdir /tmp/full +mount -t tmpfs -o size=1,nr_inodes=1 tmpfs /tmp/full + +# verb: setup +# Run this first, otherwise any encrypted credentials wouldn't be decryptable +# as we regenerate the host key +rm -fv /var/lib/systemd/credential.secret +systemd-creds setup +test -e /var/lib/systemd/credential.secret +rm -fv /var/lib/systemd/credential.secret + +# Prepare a couple of dummy credentials for the cat/list verbs +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" +chmod -R 0400 "$CRED_DIR" "$ENC_CRED_DIR" +chmod -R 0444 "$CRED_DIR/insecure" +mkdir /tmp/empty/ + +systemd-creds --system +systemd-creds --no-pager --help +systemd-creds --version +systemd-creds has-tpm2 || : +systemd-creds has-tpm2 -q || : + +# verb: list +systemd-creds list --system +ENCRYPTED_CREDENTIALS_DIRECTORY="$ENC_CRED_DIR" CREDENTIALS_DIRECTORY="$CRED_DIR" systemd-creds list --no-legend +ENCRYPTED_CREDENTIALS_DIRECTORY="$ENC_CRED_DIR" CREDENTIALS_DIRECTORY="$CRED_DIR" systemd-creds list --json=pretty | jq +ENCRYPTED_CREDENTIALS_DIRECTORY="$ENC_CRED_DIR" CREDENTIALS_DIRECTORY="$CRED_DIR" systemd-creds list --json=short | jq +ENCRYPTED_CREDENTIALS_DIRECTORY="$ENC_CRED_DIR" CREDENTIALS_DIRECTORY="$CRED_DIR" systemd-creds list --json=off +ENCRYPTED_CREDENTIALS_DIRECTORY="/tmp/empty/" CREDENTIALS_DIRECTORY="/tmp/empty/" systemd-creds list + +# verb: cat +for cred in secure-or-weak insecure encrypted encrypted-unnamed; do + ENCRYPTED_CREDENTIALS_DIRECTORY="$ENC_CRED_DIR" CREDENTIALS_DIRECTORY="$CRED_DIR" systemd-creds cat "$cred" +done +run_with_cred_compare "mycred:" "" cat mycred +run_with_cred_compare "mycred:\n" "\n" cat mycred +run_with_cred_compare "mycred:foo" "foo" cat mycred +run_with_cred_compare "mycred:foo" "foofoofoo" cat mycred mycred mycred +# Note: --newline= does nothing when stdout is not a tty, which is the case here +run_with_cred_compare "mycred:foo" "foo" --newline=yes cat mycred +run_with_cred_compare "mycred:foo" "foo" --newline=no cat mycred +run_with_cred_compare "mycred:foo" "foo" --newline=auto cat mycred +run_with_cred_compare "mycred:foo" "foo" --transcode=no cat mycred +run_with_cred_compare "mycred:foo" "foo" --transcode=0 cat mycred +run_with_cred_compare "mycred:foo" "foo" --transcode=false cat mycred +run_with_cred_compare "mycred:foo" "Zm9v" --transcode=base64 cat mycred +run_with_cred_compare "mycred:Zm9v" "foo" --transcode=unbase64 cat mycred +run_with_cred_compare "mycred:Zm9v" "foofoofoo" --transcode=unbase64 cat mycred mycred mycred +run_with_cred_compare "mycred:Zm9vCg==" "foo\n" --transcode=unbase64 cat mycred +run_with_cred_compare "mycred:hello world" "68656c6c6f20776f726c64" --transcode=hex cat mycred +run_with_cred_compare "mycred:68656c6c6f20776f726c64" "hello world" --transcode=unhex cat mycred +run_with_cred_compare "mycred:68656c6c6f20776f726c64" "hello worldhello world" --transcode=unhex cat mycred mycred +run_with_cred_compare "mycred:68656c6c6f0a776f726c64" "hello\nworld" --transcode=unhex cat mycred +run_with_cred_compare 'mycred:{ "foo" : "bar", "baz" : [ 3, 4 ] }' '{"foo":"bar","baz":[3,4]}\n' --json=short cat mycred +systemd-run -p SetCredential='mycred:{ "foo" : "bar", "baz" : [ 3, 4 ] }' --wait --pipe -- systemd-creds --json=pretty cat mycred | jq + +# verb: encrypt/decrypt +echo "According to all known laws of aviation..." >/tmp/cred.orig +systemd-creds --with-key=host encrypt /tmp/cred.orig /tmp/cred.enc +systemd-creds decrypt /tmp/cred.enc /tmp/cred.dec +diff /tmp/cred.orig /tmp/cred.dec +rm -f /tmp/cred.{enc,dec} +# --pretty +cred_name="fo'''o''bar" +cred_option="$(systemd-creds --pretty --name="$cred_name" encrypt /tmp/cred.orig -)" +mkdir -p /run/systemd/system +cat >/run/systemd/system/test-54-pretty-cred.service <<EOF +[Service] +Type=oneshot +${cred_option:?} +ExecStart=bash -c "diff <(systemd-creds cat \"$cred_name\") /tmp/cred.orig" +EOF +systemctl daemon-reload +systemctl start test-54-pretty-cred +rm /run/systemd/system/test-54-pretty-cred.service +# Credential validation: name +systemd-creds --name="foo" -H encrypt /tmp/cred.orig /tmp/cred.enc +(! systemd-creds decrypt /tmp/cred.enc /tmp/cred.dec) +(! systemd-creds --name="bar" decrypt /tmp/cred.enc /tmp/cred.dec) +systemd-creds --name="" decrypt /tmp/cred.enc /tmp/cred.dec +diff /tmp/cred.orig /tmp/cred.dec +rm -f /tmp/cred.dec +systemd-creds --name="foo" decrypt /tmp/cred.enc /tmp/cred.dec +diff /tmp/cred.orig /tmp/cred.dec +rm -f /tmp/cred.{enc,dec} +# Credential validation: time +systemd-creds --not-after="+1d" encrypt /tmp/cred.orig /tmp/cred.enc +(! systemd-creds --timestamp="+2d" decrypt /tmp/cred.enc /tmp/cred.dec) +systemd-creds decrypt /tmp/cred.enc /tmp/cred.dec +diff /tmp/cred.orig /tmp/cred.dec +rm -f /tmp/cred.{enc,dec} + +(! unshare -m bash -exc "mount -t tmpfs tmpfs /run/credentials && systemd-creds list") +(! unshare -m bash -exc "mount -t tmpfs tmpfs /run/credentials && systemd-creds --system list") +(! CREDENTIALS_DIRECTORY="" systemd-creds list) +(! systemd-creds --system --foo) +(! systemd-creds --system -@) +(! systemd-creds --system --json=) +(! systemd-creds --system --json="") +(! systemd-creds --system --json=foo) +(! systemd-creds --system cat) +(! systemd-creds --system cat "") +(! systemd-creds --system cat this-should-not-exist) +(! systemd-run -p SetCredential=mycred:foo --wait --pipe -- systemd-creds --transcode= cat mycred) +(! systemd-run -p SetCredential=mycred:foo --wait --pipe -- systemd-creds --transcode="" cat mycred) +(! systemd-run -p SetCredential=mycred:foo --wait --pipe -- systemd-creds --transcode=foo cat mycred) +(! systemd-run -p SetCredential=mycred:foo --wait --pipe -- systemd-creds --newline=foo cat mycred) +(! systemd-run -p SetCredential=mycred:notbase64 --wait --pipe -- systemd-creds --transcode=unbase64 cat mycred) +(! systemd-run -p SetCredential=mycred:nothex --wait --pipe -- systemd-creds --transcode=unhex cat mycred) +(! systemd-run -p SetCredential=mycred:a --wait --pipe -- systemd-creds --transcode=unhex cat mycred) +(! systemd-run -p SetCredential=mycred:notjson --wait --pipe -- systemd-creds --json=short cat mycred) +(! systemd-run -p SetCredential=mycred:notjson --wait --pipe -- systemd-creds --json=pretty cat mycred) +(! systemd-creds encrypt /foo/bar/baz -) +(! systemd-creds decrypt /foo/bar/baz -) +(! systemd-creds decrypt / -) +(! systemd-creds encrypt / -) +(! echo foo | systemd-creds --with-key=foo encrypt - -) +(! echo {0..20} | systemd-creds decrypt - -) +(! systemd-creds --not-after= encrypt /tmp/cred.orig /tmp/cred.enc) +(! systemd-creds --not-after="" encrypt /tmp/cred.orig /tmp/cred.enc) +(! systemd-creds --not-after="-1d" encrypt /tmp/cred.orig /tmp/cred.enc) +(! systemd-creds --timestamp= encrypt /tmp/cred.orig /tmp/cred.enc) +(! systemd-creds --timestamp="" encrypt /tmp/cred.orig /tmp/cred.enc) +(! dd if=/dev/zero count=2M | systemd-creds --with-key=tpm2-absent encrypt - /dev/null) +(! dd if=/dev/zero count=2M | systemd-creds --with-key=tpm2-absent decrypt - /dev/null) +(! echo foo | systemd-creds encrypt - /tmp/full/foo) +(! echo foo | systemd-creds encrypt - - | systemd-creds decrypt - /tmp/full/foo) + +# Verify that the creds are properly loaded and we can read them from the service's unpriv user +systemd-run -p LoadCredential=passwd:/etc/passwd \ + -p LoadCredential=shadow:/etc/shadow \ + -p SetCredential=dog:wuff \ + -p DynamicUser=1 \ + --unit=test-54-unpriv.service \ + --wait \ + --pipe \ + cat '${CREDENTIALS_DIRECTORY}/passwd' '${CREDENTIALS_DIRECTORY}/shadow' '${CREDENTIALS_DIRECTORY}/dog' \ + >/tmp/ts54-concat +(cat /etc/passwd /etc/shadow && echo -n wuff) | cmp /tmp/ts54-concat +rm /tmp/ts54-concat + +# Test that SetCredential= acts as fallback for LoadCredential= +echo piff >/tmp/ts54-fallback +[ "$(systemd-run -p LoadCredential=paff:/tmp/ts54-fallback -p SetCredential=paff:poff --pipe --wait systemd-creds cat paff)" = "piff" ] +rm /tmp/ts54-fallback +[ "$(systemd-run -p LoadCredential=paff:/tmp/ts54-fallback -p SetCredential=paff:poff --pipe --wait systemd-creds cat paff)" = "poff" ] + +if systemd-detect-virt -q -c ; then + expected_credential=mynspawncredential + expected_value=strangevalue +elif [ -d /sys/firmware/qemu_fw_cfg/by_name ]; then + # Verify that passing creds through kernel cmdline works + [ "$(systemd-creds --system cat kernelcmdlinecred)" = "uff" ] + [ "$(systemd-creds --system cat waldi)" = "woooofffwufffwuff" ] + + # And that it also works via SMBIOS + [ "$(systemd-creds --system cat smbioscredential)" = "magicdata" ] + [ "$(systemd-creds --system cat binarysmbioscredential)" = "magicbinarydata" ] + + # If we aren't run in nspawn, we are run in qemu + systemd-detect-virt -q -v + expected_credential=myqemucredential + expected_value=othervalue + + # Verify that writing a sysctl via the kernel cmdline worked + [ "$(cat /proc/sys/kernel/domainname)" = "sysctltest" ] + + # Verify that creating a user via sysusers via the kernel cmdline worked + grep -q ^credtestuser: /etc/passwd + + # Verify that writing a file via tmpfiles worked + [ "$(cat /tmp/sourcedfromcredential)" = "tmpfilessecret" ] + [ "$(cat /etc/motd.d/50-provision.conf)" = "hello" ] + [ "$(cat /etc/issue.d/50-provision.conf)" = "welcome" ] +else + echo "qemu_fw_cfg support missing in kernel. Sniff!" + expected_credential="" + expected_value="" +fi + +if [ "$expected_credential" != "" ] ; then + # If this test is run in nspawn a credential should have been passed to us. See test/TEST-54-CREDS/test.sh + [ "$(systemd-creds --system cat "$expected_credential")" = "$expected_value" ] + + # Test that propagation from system credential to service credential works + [ "$(systemd-run -p LoadCredential="$expected_credential" --pipe --wait systemd-creds cat "$expected_credential")" = "$expected_value" ] + + # Check it also works, if we rename it while propagating it + [ "$(systemd-run -p LoadCredential=miau:"$expected_credential" --pipe --wait systemd-creds cat miau)" = "$expected_value" ] + + # Combine it with a fallback (which should have no effect, given the cred should be passed down) + [ "$(systemd-run -p LoadCredential="$expected_credential" -p SetCredential="$expected_credential":zzz --pipe --wait systemd-creds cat "$expected_credential")" = "$expected_value" ] + + # This should succeed + systemd-run -p AssertCredential="$expected_credential" -p Type=oneshot true + + # And this should fail + (! systemd-run -p AssertCredential="undefinedcredential" -p Type=oneshot true) +fi + +# Verify that the creds are immutable +(! systemd-run -p LoadCredential=passwd:/etc/passwd \ + -p DynamicUser=1 \ + --unit=test-54-immutable-touch.service \ + --wait \ + touch '${CREDENTIALS_DIRECTORY}/passwd') +(! systemd-run -p LoadCredential=passwd:/etc/passwd \ + -p DynamicUser=1 \ + --unit=test-54-immutable-rm.service \ + --wait \ + rm '${CREDENTIALS_DIRECTORY}/passwd') + +# Check directory-based loading +mkdir -p /tmp/ts54-creds/sub +echo -n a >/tmp/ts54-creds/foo +echo -n b >/tmp/ts54-creds/bar +echo -n c >/tmp/ts54-creds/baz +echo -n d >/tmp/ts54-creds/sub/qux +systemd-run -p LoadCredential=cred:/tmp/ts54-creds \ + -p DynamicUser=1 \ + --unit=test-54-dir.service \ + --wait \ + --pipe \ + cat '${CREDENTIALS_DIRECTORY}/cred_foo' \ + '${CREDENTIALS_DIRECTORY}/cred_bar' \ + '${CREDENTIALS_DIRECTORY}/cred_baz' \ + '${CREDENTIALS_DIRECTORY}/cred_sub_qux' >/tmp/ts54-concat +cmp /tmp/ts54-concat <(echo -n abcd) +rm /tmp/ts54-concat +rm -rf /tmp/ts54-creds + +# Check that globs work as expected +mkdir -p /run/credstore +echo -n a >/run/credstore/test.creds.first +echo -n b >/run/credstore/test.creds.second +mkdir -p /etc/credstore +echo -n c >/etc/credstore/test.creds.third +systemd-run -p "ImportCredential=test.creds.*" \ + --unit=test-54-ImportCredential.service \ + -p DynamicUser=1 \ + --wait \ + --pipe \ + cat '${CREDENTIALS_DIRECTORY}/test.creds.first' \ + '${CREDENTIALS_DIRECTORY}/test.creds.second' \ + '${CREDENTIALS_DIRECTORY}/test.creds.third' >/tmp/ts54-concat +cmp /tmp/ts54-concat <(echo -n abc) + +# Now test encrypted credentials (only supported when built with OpenSSL though) +if systemctl --version | grep -q -- +OPENSSL ; then + echo -n $RANDOM >/tmp/test-54-plaintext + systemd-creds encrypt --name=test-54 /tmp/test-54-plaintext /tmp/test-54-ciphertext + systemd-creds decrypt --name=test-54 /tmp/test-54-ciphertext | cmp /tmp/test-54-plaintext + + systemd-run -p LoadCredentialEncrypted=test-54:/tmp/test-54-ciphertext \ + --wait \ + --pipe \ + cat '${CREDENTIALS_DIRECTORY}/test-54' | cmp /tmp/test-54-plaintext + + echo -n $RANDOM >/tmp/test-54-plaintext + systemd-creds encrypt --name=test-54 /tmp/test-54-plaintext /tmp/test-54-ciphertext + systemd-creds decrypt --name=test-54 /tmp/test-54-ciphertext | cmp /tmp/test-54-plaintext + + systemd-run -p SetCredentialEncrypted=test-54:"$(cat /tmp/test-54-ciphertext)" \ + --wait \ + --pipe \ + cat '${CREDENTIALS_DIRECTORY}/test-54' | cmp /tmp/test-54-plaintext + + rm /tmp/test-54-plaintext /tmp/test-54-ciphertext +fi + +# https://github.com/systemd/systemd/issues/27275 +systemd-run -p DynamicUser=yes -p 'LoadCredential=os:/etc/os-release' \ + -p 'ExecStartPre=true' \ + -p 'ExecStartPre=systemd-creds cat os' \ + --unit=test-54-exec-start.service \ + --wait \ + --pipe \ + true | cmp /etc/os-release + +if ! systemd-detect-virt -q -c ; then + # Validate that the credential we inserted via the initrd logic arrived + test "$(systemd-creds cat --system myinitrdcred)" = "guatemala" + + # Check that the fstab credential logic worked + test -d /injected + grep -q /injected /proc/self/mountinfo + + # Make sure the getty generator processed the credentials properly + systemctl -P Wants show getty.target | grep -q container-getty@idontexist.service +fi + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-55-testbloat.service b/test/units/testsuite-55-testbloat.service new file mode 100644 index 0000000..6c8e3c9 --- /dev/null +++ b/test/units/testsuite-55-testbloat.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Create a lot of memory pressure + +[Service] +# A VERY small memory.high will cause the 'stress' (trying to use a lot of memory) +# to throttle and be put under heavy pressure. +MemoryHigh=3M +Slice=testsuite-55-workload.slice +ExecStart=stress --timeout 3m --vm 10 --vm-bytes 200M --vm-keep --vm-stride 1 diff --git a/test/units/testsuite-55-testchill.service b/test/units/testsuite-55-testchill.service new file mode 100644 index 0000000..369b802 --- /dev/null +++ b/test/units/testsuite-55-testchill.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=No memory pressure + +[Service] +MemoryHigh=3M +Slice=testsuite-55-workload.slice +ExecStart=sleep infinity diff --git a/test/units/testsuite-55-testmunch.service b/test/units/testsuite-55-testmunch.service new file mode 100644 index 0000000..3730059 --- /dev/null +++ b/test/units/testsuite-55-testmunch.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Create some memory pressure + +[Service] +MemoryHigh=12M +Slice=testsuite-55-workload.slice +ExecStart=stress --timeout 3m --vm 10 --vm-bytes 200M --vm-keep --vm-stride 1 diff --git a/test/units/testsuite-55-workload.slice b/test/units/testsuite-55-workload.slice new file mode 100644 index 0000000..d117b75 --- /dev/null +++ b/test/units/testsuite-55-workload.slice @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Test slice for memory pressure kills + +[Slice] +CPUAccounting=true +MemoryAccounting=true +IOAccounting=true +TasksAccounting=true +ManagedOOMMemoryPressure=kill +ManagedOOMMemoryPressureLimit=20% diff --git a/test/units/testsuite-55.service b/test/units/testsuite-55.service new file mode 100644 index 0000000..00fb499 --- /dev/null +++ b/test/units/testsuite-55.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TESTSUITE-55-OOMD +After=user@4711.service +Wants=user@4711.service + +[Service] +ExecStartPre=rm -f /failed /skipped /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-55.sh b/test/units/testsuite-55.sh new file mode 100755 index 0000000..81617db --- /dev/null +++ b/test/units/testsuite-55.sh @@ -0,0 +1,182 @@ +#!/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 + +systemd-analyze log-level debug + +# Ensure that the init.scope.d drop-in is applied on boot +test "$(cat /sys/fs/cgroup/init.scope/memory.high)" != "max" + +# Loose checks to ensure the environment has the necessary features for systemd-oomd +[[ -e /proc/pressure ]] || echo "no PSI" >>/skipped +[[ "$(get_cgroup_hierarchy)" == "unified" ]] || echo "no cgroupsv2" >>/skipped +[[ -x /usr/lib/systemd/systemd-oomd ]] || echo "no oomd" >>/skipped +if [[ -s /skipped ]]; then + exit 0 +fi + +rm -rf /run/systemd/system/testsuite-55-testbloat.service.d + +# Activate swap file if we are in a VM +if systemd-detect-virt --vm --quiet; then + if [[ "$(findmnt -n -o FSTYPE /)" == btrfs ]]; then + btrfs filesystem mkswapfile -s 64M /swapfile + else + dd if=/dev/zero of=/swapfile bs=1M count=64 + chmod 0600 /swapfile + mkswap /swapfile + fi + + swapon /swapfile + swapon --show +fi + +# Configure oomd explicitly to avoid conflicts with distro dropins +mkdir -p /run/systemd/oomd.conf.d/ +cat >/run/systemd/oomd.conf.d/99-oomd-test.conf <<EOF +[OOM] +DefaultMemoryPressureDurationSec=2s +EOF + +mkdir -p /run/systemd/system/-.slice.d/ +cat >/run/systemd/system/-.slice.d/99-oomd-test.conf <<EOF +[Slice] +ManagedOOMSwap=auto +EOF + +mkdir -p /run/systemd/system/user@.service.d/ +cat >/run/systemd/system/user@.service.d/99-oomd-test.conf <<EOF +[Service] +ManagedOOMMemoryPressure=auto +ManagedOOMMemoryPressureLimit=0% +EOF + +mkdir -p /run/systemd/system/systemd-oomd.service.d/ +cat >/run/systemd/system/systemd-oomd.service.d/debug.conf <<EOF +[Service] +Environment=SYSTEMD_LOG_LEVEL=debug +EOF + +systemctl daemon-reload + +# enable the service to ensure dbus-org.freedesktop.oom1.service exists +# and D-Bus activation works +systemctl enable systemd-oomd.service + +# if oomd is already running for some reasons, then restart it to make sure the above settings to be applied +if systemctl is-active systemd-oomd.service; then + systemctl restart systemd-oomd.service +fi + +if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then + # If we're running with sanitizers, sd-executor might pull in quite a significant chunk of shared + # libraries, which in turn causes a lot of pressure that can put us in the front when sd-oomd decides to + # go on a killing spree. This fact is exacerbated further on Arch Linux which ships unstripped gcc-libs, + # so sd-executor pulls in over 30M of libs on startup. Let's make the MemoryHigh= limit a bit more + # generous when running with sanitizers to make the test happy. + mkdir -p /run/systemd/system/testsuite-55-testchill.service.d/ + cat >/run/systemd/system/testsuite-55-testchill.service.d/99-MemoryHigh.conf <<EOF +[Service] +MemoryHigh=60M +EOF + # Do the same for the user instance as well + mkdir -p /run/systemd/user/ + cp -rfv /run/systemd/system/testsuite-55-testchill.service.d/ /run/systemd/user/ +else + # Ensure that we can start services even with a very low hard memory cap without oom-kills, but skip + # under sanitizers as they balloon memory usage. + systemd-run -t -p MemoryMax=10M -p MemorySwapMax=0 -p MemoryZSwapMax=0 /bin/true +fi + +systemctl start testsuite-55-testchill.service +systemctl start testsuite-55-testbloat.service + +# Verify systemd-oomd is monitoring the expected units +timeout 1m bash -xec 'until oomctl | grep "/testsuite-55-workload.slice"; do sleep 1; done' +oomctl | grep "/testsuite-55-workload.slice" +oomctl | grep "20.00%" +oomctl | grep "Default Memory Pressure Duration: 2s" + +systemctl status testsuite-55-testchill.service + +# systemd-oomd watches for elevated pressure for 2 seconds before acting. +# It can take time to build up pressure so either wait 2 minutes or for the service to fail. +for _ in {0..59}; do + if ! systemctl status testsuite-55-testbloat.service; then + break + fi + oomctl + sleep 2 +done + +# testbloat should be killed and testchill should be fine +if systemctl status testsuite-55-testbloat.service; then exit 42; fi +if ! systemctl status testsuite-55-testchill.service; then exit 24; fi + +# Make sure we also work correctly on user units. +loginctl enable-linger testuser + +systemctl start --machine "testuser@.host" --user testsuite-55-testchill.service +systemctl start --machine "testuser@.host" --user testsuite-55-testbloat.service + +# Verify systemd-oomd is monitoring the expected units +# Try to avoid racing the oomctl output check by checking in a loop with a timeout +timeout 1m bash -xec 'until oomctl | grep "/testsuite-55-workload.slice"; do sleep 1; done' +oomctl | grep -E "/user.slice.*/testsuite-55-workload.slice" +oomctl | grep "20.00%" +oomctl | grep "Default Memory Pressure Duration: 2s" + +systemctl --machine "testuser@.host" --user status testsuite-55-testchill.service + +# systemd-oomd watches for elevated pressure for 2 seconds before acting. +# It can take time to build up pressure so either wait 2 minutes or for the service to fail. +for _ in {0..59}; do + if ! systemctl --machine "testuser@.host" --user status testsuite-55-testbloat.service; then + break + fi + oomctl + sleep 2 +done + +# testbloat should be killed and testchill should be fine +if systemctl --machine "testuser@.host" --user status testsuite-55-testbloat.service; then exit 42; fi +if ! systemctl --machine "testuser@.host" --user status testsuite-55-testchill.service; then exit 24; fi + +loginctl disable-linger testuser + +# only run this portion of the test if we can set xattrs +if cgroupfs_supports_user_xattrs; then + sleep 120 # wait for systemd-oomd kill cool down and elevated memory pressure to come down + + mkdir -p /run/systemd/system/testsuite-55-testbloat.service.d/ + cat >/run/systemd/system/testsuite-55-testbloat.service.d/override.conf <<EOF +[Service] +ManagedOOMPreference=avoid +EOF + + systemctl daemon-reload + systemctl start testsuite-55-testchill.service + systemctl start testsuite-55-testmunch.service + systemctl start testsuite-55-testbloat.service + + for _ in {0..59}; do + if ! systemctl status testsuite-55-testmunch.service; then + break + fi + oomctl + sleep 2 + done + + # testmunch should be killed since testbloat had the avoid xattr on it + if ! systemctl status testsuite-55-testbloat.service; then exit 25; fi + if systemctl status testsuite-55-testmunch.service; then exit 43; fi + if ! systemctl status testsuite-55-testchill.service; then exit 24; fi +fi + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-58.service b/test/units/testsuite-58.service new file mode 100644 index 0000000..f843527 --- /dev/null +++ b/test/units/testsuite-58.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-58-REPART + +[Service] +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-58.sh b/test/units/testsuite-58.sh new file mode 100755 index 0000000..c64b203 --- /dev/null +++ b/test/units/testsuite-58.sh @@ -0,0 +1,1307 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2317 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +if ! command -v systemd-repart >/dev/null; then + echo "no systemd-repart" >/skipped + exit 0 +fi + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +export SYSTEMD_LOG_LEVEL=debug +export PAGER=cat + +# Disable use of special glyphs such as → +export SYSTEMD_UTF8=0 + +seed=750b6cd5c4ae4012a15e7be3c29e6a47 + +if ! systemd-detect-virt --quiet --container; then + udevadm control --log-level debug +fi + +machine="$(uname -m)" +if [ "${machine}" = "x86_64" ]; then + root_guid=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709 + root_uuid=60F33797-1D71-4DCB-AA6F-20564F036CD0 + root_uuid2=73A4CCD2-EAF5-44DA-A366-F99188210FDC + usr_guid=8484680C-9521-48C6-9C11-B0720656F69E + usr_uuid=7E3369DD-D653-4513-ADF5-B993A9F20C16 + architecture="x86-64" +elif [ "${machine}" = "i386" ] || [ "${machine}" = "i686" ] || [ "${machine}" = "x86" ]; then + root_guid=44479540-F297-41B2-9AF7-D131D5F0458A + root_uuid=02B4253F-29A4-404E-8972-1669D3B03C87 + root_uuid2=268E0FD3-B468-4806-A823-E533FE9BB9CC + usr_guid=75250D76-8CC6-458E-BD66-BD47CC81A812 + usr_uuid=7B42FFB0-B0E1-4395-B20B-C78F4A571648 + architecture="x86" +elif [ "${machine}" = "aarch64" ] || [ "${machine}" = "aarch64_be" ] || [ "${machine}" = "armv8b" ] || [ "${machine}" = "armv8l" ]; then + root_guid=B921B045-1DF0-41C3-AF44-4C6F280D3FAE + root_uuid=055D0227-53A6-4033-85C3-9A5973EFF483 + root_uuid2=F7DBBE48-8FD0-4833-8411-AA34E7C8E60A + usr_guid=B0E01050-EE5F-4390-949A-9101B17104E9 + usr_uuid=FCE3C75E-D6A4-44C0-87F0-4C105183FB1F + architecture="arm64" +elif [ "${machine}" = "arm" ]; then + root_guid=69DAD710-2CE4-4E3C-B16C-21A1D49ABED3 + root_uuid=567DA89E-8DE2-4499-8D10-18F212DFF034 + root_uuid2=813ECFE5-4C89-4193-8A52-437493F2F96E + usr_guid=7D0359A3-02B3-4F0A-865C-654403E70625 + usr_uuid=71E93DC2-5073-42CB-8A84-A354E64D8966 + architecture="arm" +elif [ "${machine}" = "loongarch64" ]; then + root_guid=77055800-792C-4F94-B39A-98C91B762BB6 + root_uuid=D8EFC2D2-0133-41E4-BDCB-3B9F4CFDDDE8 + root_uuid2=36499F9E-0688-40C1-A746-EA8FD9543C56 + usr_guid=E611C702-575C-4CBE-9A46-434FA0BF7E3F + usr_uuid=031FFA75-00BB-49B6-A70D-911D2D82A5B7 + architecture="loongarch64" +elif [ "${machine}" = "ia64" ]; then + root_guid=993D8D3D-F80E-4225-855A-9DAF8ED7EA97 + root_uuid=DCF33449-0896-4EA9-BC24-7D58AEEF522D + root_uuid2=C2A6CAB7-ABEA-4FBA-8C48-CB4C52E6CA38 + usr_guid=4301D2A6-4E3B-4B2A-BB94-9E0B2C4225EA + usr_uuid=BC2BCCE7-80D6-449A-85CC-637424CE5241 + architecture="ia64" +elif [ "${machine}" = "s390x" ]; then + root_guid=5EEAD9A9-FE09-4A1E-A1D7-520D00531306 + root_uuid=7EBE0C85-E27E-48EC-B164-F4807606232E + root_uuid2=2A074E1C-2A19-4094-A0C2-24B1A5D52FCB + usr_guid=8A4F5770-50AA-4ED3-874A-99B710DB6FEA + usr_uuid=51171D30-35CF-4A49-B8B5-9478B9B796A5 + architecture="s390x" +elif [ "${machine}" = "ppc64le" ]; then + root_guid=C31C45E6-3F39-412E-80FB-4809C4980599 + root_uuid=061E67A1-092F-482F-8150-B525D50D6654 + root_uuid2=A6687CEF-4E4F-44E7-90B3-CDA52EA81739 + usr_guid=15BB03AF-77E7-4D4A-B12B-C0D084F7491C + usr_uuid=C0D0823B-8040-4C7C-A629-026248E297FB + architecture="ppc64-le" +else + echo "Unexpected uname -m: ${machine} in testsuite-58.sh, please fix me" + exit 1 +fi + +testcase_basic() { + local defs imgs output + local loop volume + + 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 "*** 1. create an empty image ***" + + systemd-repart --offline="$OFFLINE" \ + --empty=create \ + --size=1G \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 2097118" + + echo "*** 2. Testing with root, root2, home, and swap ***" + + tee "$defs/root.conf" <<EOF +[Partition] +Type=root +EOF + + ln -s root.conf "$defs/root2.conf" + + tee "$defs/home.conf" <<EOF +[Partition] +Type=home +Label=home-first +Label=home-always-too-long-xxxxxxxxxxxxxx-%v +EOF + + tee "$defs/swap.conf" <<EOF +[Partition] +Type=swap +SizeMaxBytes=64M +PaddingMinBytes=92M +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --dry-run=no \ + --seed="$seed" \ + --include-partitions=home,swap \ + --offline="$OFFLINE" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 2097118 +$imgs/zzz1 : start= 2048, size= 1775576, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/zzz2 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\"" + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=50M \ + --seed="$seed" \ + --include-partitions=root,home \ + "$imgs/qqq" + + sfdisk -d "$imgs/qqq" | grep -v -e 'sector-size' -e '^$' + + systemd-repart --offline="$OFFLINE" \ + --empty=create \ + --size=1G \ + --dry-run=no \ + --seed="$seed" \ + --definitions "" \ + --copy-from="$imgs/qqq" \ + --copy-from="$imgs/qqq" \ + "$imgs/copy" + + output=$(sfdisk -d "$imgs/copy" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/copy +unit: sectors +first-lba: 2048 +last-lba: 2097118 +$imgs/copy1 : start= 2048, size= 33432, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/copy2 : start= 35480, size= 33440, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/copy3 : start= 68920, size= 33440, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\" +$imgs/copy4 : start= 102360, size= 33432, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/copy5 : start= 135792, size= 33440, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/copy6 : start= 169232, size= 33440, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\"" + + rm "$imgs/qqq" "$imgs/copy" # Save disk space + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --dry-run=no \ + --seed="$seed" \ + --empty=force \ + --defer-partitions=home,root \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 2097118 +$imgs/zzz4 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\"" + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --dry-run=no \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 2097118 +$imgs/zzz1 : start= 2048, size= 591856, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/zzz2 : start= 593904, size= 591856, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/zzz3 : start= 1185760, size= 591864, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\" +$imgs/zzz4 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\"" + + echo "*** 3. Testing with root, root2, home, swap, and another partition ***" + + tee "$defs/swap.conf" <<EOF +[Partition] +Type=swap +SizeMaxBytes=64M +EOF + + tee "$defs/extra.conf" <<EOF +[Partition] +Type=linux-generic +Label=custom_label +UUID=a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +EOF + + echo "Label=ignored_label" >>"$defs/home.conf" + echo "UUID=b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" >>"$defs/home.conf" + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --dry-run=no \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 2097118 +$imgs/zzz1 : start= 2048, size= 591856, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/zzz2 : start= 593904, size= 591856, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/zzz3 : start= 1185760, size= 591864, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\" +$imgs/zzz4 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\" +$imgs/zzz5 : start= 1908696, size= 188416, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=A0A1A2A3-A4A5-A6A7-A8A9-AAABACADAEAF, name=\"custom_label\"" + + echo "*** 4. Resizing to 2G ***" + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --size=2G \ + --dry-run=no \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 4194270 +$imgs/zzz1 : start= 2048, size= 591856, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/zzz2 : start= 593904, size= 591856, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/zzz3 : start= 1185760, size= 591864, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\" +$imgs/zzz4 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\" +$imgs/zzz5 : start= 1908696, size= 2285568, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=A0A1A2A3-A4A5-A6A7-A8A9-AAABACADAEAF, name=\"custom_label\"" + + echo "*** 5. Testing with root, root2, home, swap, another partition, and partition copy ***" + + dd if=/dev/urandom of="$imgs/block-copy" bs=4096 count=10240 + + tee "$defs/extra2.conf" <<EOF +[Partition] +Type=linux-generic +Label=block-copy +UUID=2a1d97e1d0a346cca26eadc643926617 +CopyBlocks=$imgs/block-copy +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --size=3G \ + --dry-run=no \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 6291422 +$imgs/zzz1 : start= 2048, size= 591856, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/zzz2 : start= 593904, size= 591856, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/zzz3 : start= 1185760, size= 591864, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\" +$imgs/zzz4 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\" +$imgs/zzz5 : start= 1908696, size= 2285568, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=A0A1A2A3-A4A5-A6A7-A8A9-AAABACADAEAF, name=\"custom_label\" +$imgs/zzz6 : start= 4194264, size= 2097152, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=2A1D97E1-D0A3-46CC-A26E-ADC643926617, name=\"block-copy\"" + + cmp --bytes=$((4096*10240)) --ignore-initial=0:$((512*4194264)) "$imgs/block-copy" "$imgs/zzz" + + echo "*** 6. Testing Format=/Encrypt=/CopyFiles= ***" + + tee "$defs/extra3.conf" <<EOF +[Partition] +Type=linux-generic +Label=luks-format-copy +UUID=7b93d1f2-595d-4ce3-b0b9-837fbd9e63b0 +Format=ext4 +Encrypt=yes +CopyFiles=$defs:/def +SizeMinBytes=48M +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --size=auto \ + --dry-run=no \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk -d "$imgs/zzz" | grep -v -e 'sector-size' -e '^$') + + assert_eq "$output" "label: gpt +label-id: 1D2CE291-7CCE-4F7D-BC83-FDB49AD74EBD +device: $imgs/zzz +unit: sectors +first-lba: 2048 +last-lba: 6389726 +$imgs/zzz1 : start= 2048, size= 591856, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915, uuid=4980595D-D74A-483A-AA9E-9903879A0EE5, name=\"home-first\", attrs=\"GUID:59\" +$imgs/zzz2 : start= 593904, size= 591856, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\" +$imgs/zzz3 : start= 1185760, size= 591864, type=${root_guid}, uuid=${root_uuid2}, name=\"root-${architecture}-2\", attrs=\"GUID:59\" +$imgs/zzz4 : start= 1777624, size= 131072, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, uuid=78C92DB8-3D2B-4823-B0DC-792B78F66F1E, name=\"swap\" +$imgs/zzz5 : start= 1908696, size= 2285568, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=A0A1A2A3-A4A5-A6A7-A8A9-AAABACADAEAF, name=\"custom_label\" +$imgs/zzz6 : start= 4194264, size= 2097152, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=2A1D97E1-D0A3-46CC-A26E-ADC643926617, name=\"block-copy\" +$imgs/zzz7 : start= 6291416, size= 98304, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=7B93D1F2-595D-4CE3-B0B9-837FBD9E63B0, name=\"luks-format-copy\"" + + if systemd-detect-virt --quiet --container; then + echo "Skipping encrypt mount tests in container." + return + fi + + loop="$(losetup -P --show --find "$imgs/zzz")" + udevadm wait --timeout 60 --settle "${loop:?}" + + volume="test-repart-$RANDOM" + + touch "$imgs/empty-password" + cryptsetup open --type=luks2 --key-file="$imgs/empty-password" "${loop}p7" "$volume" + mkdir -p "$imgs/mount" + mount -t ext4 "/dev/mapper/$volume" "$imgs/mount" + # Use deferred closing on the mapper and autoclear on the loop, so they are cleaned up on umount + cryptsetup close --deferred "$volume" + losetup -d "$loop" + diff -r "$imgs/mount/def" "$defs" >/dev/null + umount "$imgs/mount" +} + +testcase_dropin() { + local defs imgs output + + 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=swap +SizeMaxBytes=64M +UUID=837c3d67-21b3-478e-be82-7e7f83bf96d3 +EOF + + mkdir -p "$defs/root.conf.d" + tee "$defs/root.conf.d/override1.conf" <<EOF +[Partition] +Label=label1 +SizeMaxBytes=32M +EOF + + tee "$defs/root.conf.d/override2.conf" <<EOF +[Partition] +Label=label2 +EOF + + output=$(systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=100M \ + --json=pretty \ + "$imgs/zzz") + + diff -u <(echo "$output") - <<EOF +[ + { + "type" : "swap", + "label" : "label2", + "uuid" : "837c3d67-21b3-478e-be82-7e7f83bf96d3", + "partno" : 0, + "file" : "$defs/root.conf", + "node" : "$imgs/zzz1", + "offset" : 1048576, + "old_size" : 0, + "raw_size" : 33554432, + "size" : "-> 32.0M", + "old_padding" : 0, + "raw_padding" : 0, + "padding" : "-> 0B", + "activity" : "create", + "drop-in_files" : [ + "$defs/root.conf.d/override1.conf", + "$defs/root.conf.d/override2.conf" + ] + } +] +EOF +} + +testcase_multiple_definitions() { + local defs imgs output + + 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" + + mkdir -p "$defs/1" + tee "$defs/1/root1.conf" <<EOF +[Partition] +Type=swap +SizeMaxBytes=32M +UUID=7b93d1f2-595d-4ce3-b0b9-837fbd9e63b0 +Label=label1 +EOF + + mkdir -p "$defs/2" + tee "$defs/2/root2.conf" <<EOF +[Partition] +Type=swap +SizeMaxBytes=32M +UUID=837c3d67-21b3-478e-be82-7e7f83bf96d3 +Label=label2 +EOF + + output=$(systemd-repart --offline="$OFFLINE" \ + --definitions="$defs/1" \ + --definitions="$defs/2" \ + --empty=create \ + --size=100M \ + --json=pretty \ + "$imgs/zzz") + + diff -u <(echo "$output") - <<EOF +[ + { + "type" : "swap", + "label" : "label1", + "uuid" : "7b93d1f2-595d-4ce3-b0b9-837fbd9e63b0", + "partno" : 0, + "file" : "$defs/1/root1.conf", + "node" : "$imgs/zzz1", + "offset" : 1048576, + "old_size" : 0, + "raw_size" : 33554432, + "size" : "-> 32.0M", + "old_padding" : 0, + "raw_padding" : 0, + "padding" : "-> 0B", + "activity" : "create" + }, + { + "type" : "swap", + "label" : "label2", + "uuid" : "837c3d67-21b3-478e-be82-7e7f83bf96d3", + "partno" : 1, + "file" : "$defs/2/root2.conf", + "node" : "$imgs/zzz2", + "offset" : 34603008, + "old_size" : 0, + "raw_size" : 33554432, + "size" : "-> 32.0M", + "old_padding" : 0, + "raw_padding" : 0, + "padding" : "-> 0B", + "activity" : "create" + } +] +EOF +} + +testcase_copy_blocks() { + local defs imgs output + + 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 "*** First, create a disk image and verify its in order ***" + + tee "$defs/esp.conf" <<EOF +[Partition] +Type=esp +SizeMinBytes=10M +Format=vfat +EOF + + tee "$defs/usr.conf" <<EOF +[Partition] +Type=usr-${architecture} +SizeMinBytes=10M +Format=ext4 +ReadOnly=yes +EOF + + tee "$defs/root.conf" <<EOF +[Partition] +Type=root-${architecture} +SizeMinBytes=10M +Format=ext4 +MakeDirectories=/usr /efi +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=auto \ + --seed="$seed" \ + "$imgs/zzz" + + output=$(sfdisk --dump "$imgs/zzz") + + assert_in "$imgs/zzz1 : start= 2048, size= 20480, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=39107B09-615D-48FB-BA37-C663885FCE67, name=\"esp\"" "$output" + assert_in "$imgs/zzz2 : start= 22528, size= 20480, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\"" "$output" + assert_in "$imgs/zzz3 : start= 43008, size= 20480, type=${usr_guid}, uuid=${usr_uuid}, name=\"usr-${architecture}\", attrs=\"GUID:60\"" "$output" + + if systemd-detect-virt --quiet --container; then + echo "Skipping second part of copy blocks tests in container." + return + fi + + echo "*** Second, create another image with CopyBlocks=auto ***" + + tee "$defs/esp.conf" <<EOF +[Partition] +Type=esp +CopyBlocks=auto +EOF + + tee "$defs/usr.conf" <<EOF +[Partition] +Type=usr-${architecture} +ReadOnly=yes +CopyBlocks=auto +EOF + + tee "$defs/root.conf" <<EOF +[Partition] +Type=root-${architecture} +CopyBlocks=auto +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=auto \ + --seed="$seed" \ + --image="$imgs/zzz" \ + "$imgs/yyy" + + cmp "$imgs/zzz" "$imgs/yyy" +} + +testcase_unaligned_partition() { + local defs imgs output + + 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 "*** Operate on an image with unaligned partition ***" + + tee "$defs/root.conf" <<EOF +[Partition] +Type=root-${architecture} +EOF + + truncate -s 10g "$imgs/unaligned" + sfdisk "$imgs/unaligned" <<EOF +label: gpt + +start=2048, size=69044 +start=71092, size=3591848 +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + "$imgs/unaligned" + + output=$(sfdisk --dump "$imgs/unaligned") + + assert_in "$imgs/unaligned1 : start= 2048, size= 69044," "$output" + assert_in "$imgs/unaligned2 : start= 71092, size= 3591848," "$output" + assert_in "$imgs/unaligned3 : start= 3662944, size= 17308536, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\"" "$output" +} + +testcase_issue_21817() { + local defs imgs output + + 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 "*** testcase for #21817 ***" + + tee "$defs/test.conf" <<EOF +[Partition] +Type=root +EOF + + truncate -s 100m "$imgs/21817.img" + sfdisk "$imgs/21817.img" <<EOF +label: gpt + +size=50M, type=${root_guid} +, +EOF + + systemd-repart --offline="$OFFLINE" \ + --pretty=yes \ + --definitions "$imgs" \ + --seed="$seed" \ + --dry-run=no \ + "$imgs/21817.img" + + output=$(sfdisk --dump "$imgs/21817.img") + + assert_in "$imgs/21817.img1 : start= 2048, size= 102400, type=${root_guid}," "$output" + # Accept both unpadded (pre-v2.38 util-linux) and padded (v2.38+ util-linux) sizes + assert_in "$imgs/21817.img2 : start= 104448, size= (100319| 98304)," "$output" +} + +testcase_issue_24553() { + local defs imgs output + + 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 "*** testcase for #24553 ***" + + tee "$defs/root.conf" <<EOF +[Partition] +Type=root +SizeMinBytes=10G +SizeMaxBytes=120G +EOF + + tee "$imgs/partscript" <<EOF +label: gpt +label-id: C9FFE979-A415-C449-B729-78C7AA664B10 +unit: sectors +first-lba: 40 + +start=40, size=524288, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=F2E89C8A-DC5D-4C4C-A29C-6CFB643B74FD, name="ESP System Partition" +start=524328, size=14848000, type=${root_guid}, uuid=${root_uuid}, name="root-${architecture}" +EOF + + echo "*** 1. Operate on a small image compared with SizeMinBytes= ***" + truncate -s 8g "$imgs/zzz" + sfdisk "$imgs/zzz" <"$imgs/partscript" + + # This should fail, but not trigger assertions. + assert_rc 1 systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + "$imgs/zzz" + + output=$(sfdisk --dump "$imgs/zzz") + assert_in "$imgs/zzz2 : start= 524328, size= 14848000, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\"" "$output" + + echo "*** 2. Operate on an larger image compared with SizeMinBytes= ***" + rm -f "$imgs/zzz" + truncate -s 12g "$imgs/zzz" + sfdisk "$imgs/zzz" <"$imgs/partscript" + + # This should succeed. + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + "$imgs/zzz" + + output=$(sfdisk --dump "$imgs/zzz") + assert_in "$imgs/zzz2 : start= 524328, size= 24641456, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\"" "$output" + + echo "*** 3. Multiple partitions with Priority= (small disk) ***" + tee "$defs/root.conf" <<EOF +[Partition] +Type=root +SizeMinBytes=10G +SizeMaxBytes=120G +Priority=100 +EOF + + tee "$defs/usr.conf" <<EOF +[Partition] +Type=usr +SizeMinBytes=10M +Priority=10 +EOF + + rm -f "$imgs/zzz" + truncate -s 8g "$imgs/zzz" + sfdisk "$imgs/zzz" <"$imgs/partscript" + + # This should also succeed, but root is not extended. + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + "$imgs/zzz" + + output=$(sfdisk --dump "$imgs/zzz") + assert_in "$imgs/zzz2 : start= 524328, size= 14848000, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\"" "$output" + assert_in "$imgs/zzz3 : start= 15372328, size= 1404848, type=${usr_guid}, uuid=${usr_uuid}, name=\"usr-${architecture}\", attrs=\"GUID:59\"" "$output" + + echo "*** 4. Multiple partitions with Priority= (large disk) ***" + rm -f "$imgs/zzz" + truncate -s 12g "$imgs/zzz" + sfdisk "$imgs/zzz" <"$imgs/partscript" + + # This should also succeed, and root is extended. + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + "$imgs/zzz" + + output=$(sfdisk --dump "$imgs/zzz") + assert_in "$imgs/zzz2 : start= 524328, size= 20971520, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\"" "$output" + assert_in "$imgs/zzz3 : start= 21495848, size= 3669936, type=${usr_guid}, uuid=${usr_uuid}, name=\"usr-${architecture}\", attrs=\"GUID:59\"" "$output" +} + +testcase_zero_uuid() { + local defs imgs output + + 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 "*** Test image with zero UUID ***" + + tee "$defs/root.conf" <<EOF +[Partition] +Type=root-${architecture} +UUID=null +EOF + + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + --empty=create \ + --size=auto \ + "$imgs/zero" + + output=$(sfdisk --dump "$imgs/zero") + + assert_in "$imgs/zero1 : start= 2048, size= 20480, type=${root_guid}, uuid=00000000-0000-0000-0000-000000000000" "$output" +} + +testcase_verity() { + local defs imgs output + + 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 ***" + + tee "$defs/verity-data.conf" <<EOF +[Partition] +Type=root-${architecture} +CopyFiles=${defs} +Verity=data +VerityMatchKey=root +Minimize=guess +EOF + + tee "$defs/verity-hash.conf" <<EOF +[Partition] +Type=root-${architecture}-verity +Verity=hash +VerityMatchKey=root +Minimize=yes +EOF + + tee "$defs/verity-sig.conf" <<EOF +[Partition] +Type=root-${architecture}-verity-sig +Verity=signature +VerityMatchKey=root +EOF + + # Unfortunately OpenSSL insists on reading some config file, hence provide one with mostly placeholder contents + tee >"$defs/verity.openssl.cnf" <<EOF +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +C = DE +ST = Test State +L = Test Locality +O = Org Name +OU = Org Unit Name +CN = Common Name +emailAddress = test@email.com +EOF + + openssl req \ + -config "$defs/verity.openssl.cnf" \ + -new -x509 \ + -newkey rsa:1024 \ + -keyout "$defs/verity.key" \ + -out "$defs/verity.crt" \ + -days 365 \ + -nodes + + mkdir -p /run/verity.d + ln -sf "$defs/verity.crt" /run/verity.d/ok.crt + + output=$(systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + --empty=create \ + --size=auto \ + --json=pretty \ + --private-key="$defs/verity.key" \ + --certificate="$defs/verity.crt" \ + "$imgs/verity") + + drh=$(jq -r ".[] | select(.type == \"root-${architecture}\") | .roothash" <<<"$output") + hrh=$(jq -r ".[] | select(.type == \"root-${architecture}-verity\") | .roothash" <<<"$output") + srh=$(jq -r ".[] | select(.type == \"root-${architecture}-verity-sig\") | .roothash" <<<"$output") + + assert_eq "$drh" "$hrh" + assert_eq "$hrh" "$srh" + + # Check that we can dissect, mount and unmount a repart verity image. (and that the image UUID is deterministic) + + if systemd-detect-virt --quiet --container; then + echo "Skipping verity test dissect part in container." + return + fi + + systemd-dissect "$imgs/verity" --root-hash "$drh" + systemd-dissect "$imgs/verity" --root-hash "$drh" --json=short | grep -q '"imageUuid":"1d2ce291-7cce-4f7d-bc83-fdb49ad74ebd"' + systemd-dissect "$imgs/verity" --root-hash "$drh" -M "$imgs/mnt" + systemd-dissect -U "$imgs/mnt" +} + +testcase_verity_explicit_block_size() { + local defs imgs loop + + if systemd-detect-virt --quiet --container; then + echo "Skipping verity block size tests 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 "*** varying-dm-verity-block-sizes ***" + + tee "$defs/verity-data.conf" <<EOF +[Partition] +Type=root-${architecture} +CopyFiles=${defs} +Verity=data +VerityMatchKey=root +Minimize=guess +EOF + + tee "$defs/verity-hash.conf" <<EOF +[Partition] +Type=root-${architecture}-verity +Verity=hash +VerityMatchKey=root +VerityHashBlockSizeBytes=1024 +VerityDataBlockSizeBytes=4096 +Minimize=yes +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:?}" + + # Check that the verity block sizes are as expected + veritysetup dump "${loop}p2" | grep 'Data block size:' | grep -q '4096' + veritysetup dump "${loop}p2" | grep 'Hash block size:' | grep -q '1024' +} + +testcase_exclude_files() { + local defs imgs root output + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + root="$(mktemp --directory "/var/tmp/test-repart.root.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs' '$root'" RETURN + chmod 0755 "$defs" + + echo "*** file exclusion ***" + + touch "$root/abc" + mkdir "$root/usr" + touch "$root/usr/def" + touch "$root/usr/qed" + mkdir "$root/tmp" + touch "$root/tmp/prs" + mkdir "$root/proc" + touch "$root/proc/prs" + mkdir "$root/zzz" + mkdir "$root/zzz/usr" + touch "$root/zzz/usr/prs" + mkdir "$root/zzz/proc" + touch "$root/zzz/proc/prs" + + tee "$defs/00-root.conf" <<EOF +[Partition] +Type=root-${architecture} +CopyFiles=/ +CopyFiles=/zzz:/ +CopyFiles=/:/oiu +ExcludeFilesTarget=/oiu/usr +EOF + + tee "$defs/10-usr.conf" <<EOF +[Partition] +Type=usr-${architecture} +CopyFiles=/usr:/ +ExcludeFiles=/usr/qed +EOF + + output=$(systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + --empty=create \ + --size=auto \ + --json=pretty \ + --root="$root" \ + "$imgs/zzz") + + if systemd-detect-virt --quiet --container; then + echo "Skipping issue 24786 test loop/mount parts in container." + return + fi + + loop=$(losetup -P --show -f "$imgs/zzz") + udevadm wait --timeout 60 --settle "${loop:?}" + + # Test that /usr/def did not end up in the root partition but other files did. + mkdir "$imgs/mnt" + mount -t ext4 "${loop}p1" "$imgs/mnt" + assert_rc 0 ls "$imgs/mnt/abc" + assert_rc 0 ls "$imgs/mnt/usr" + assert_rc 2 ls "$imgs/mnt/usr/def" + + # Test that /zzz/usr/prs did not end up in the root partition under /usr but did end up in /zzz/usr/prs + assert_rc 2 ls "$imgs/mnt/usr/prs" + assert_rc 0 ls "$imgs/mnt/zzz/usr/prs" + + # Test that /tmp/prs did not end up in the root partition but /tmp did. + assert_rc 0 ls "$imgs/mnt/tmp" + assert_rc 2 ls "$imgs/mnt/tmp/prs" + + # Test that /usr/qed did not end up in the usr partition but /usr/def did. + mount -t ext4 "${loop}p2" "$imgs/mnt/usr" + assert_rc 0 ls "$imgs/mnt/usr/def" + assert_rc 2 ls "$imgs/mnt/usr/qed" + + # Test that /zzz/proc/prs did not end up in the root partition but /proc did. + assert_rc 0 ls "$imgs/mnt/proc" + assert_rc 2 ls "$imgs/mnt/proc/prs" + + # Test that /zzz/usr/prs did not end up in the usr partition. + assert_rc 2 ls "$imgs/mnt/usr/prs" + + # Test that /oiu/ and /oiu/zzz ended up in the root partition but /oiu/usr did not. + assert_rc 0 ls "$imgs/mnt/oiu" + assert_rc 0 ls "$imgs/mnt/oiu/zzz" + assert_rc 2 ls "$imgs/mnt/oiu/usr" + + umount -R "$imgs/mnt" + losetup -d "$loop" +} + +testcase_minimize() { + local defs imgs output + + if systemd-detect-virt --quiet --container; then + echo "Skipping minimize test in container." + return + fi + + echo "*** minimization ***" + + 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 + + for format in ext4 vfat erofs; do + if ! command -v "mkfs.$format" >/dev/null; then + continue + fi + + tee "$defs/root-$format.conf" <<EOF +[Partition] +Type=root-${architecture} +Format=${format} +CopyFiles=${defs} +Minimize=guess +EOF + done + + if command -v mksquashfs >/dev/null; then + tee "$defs/root-squashfs.conf" <<EOF +[Partition] +Type=root-${architecture} +Format=squashfs +CopyFiles=${defs} +Minimize=best +EOF + fi + + output=$(systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + --empty=create \ + --size=auto \ + --json=pretty \ + "$imgs/zzz") + + # Check that we can dissect, mount and unmount a minimized image. + + systemd-dissect "$imgs/zzz" + systemd-dissect "$imgs/zzz" -M "$imgs/mnt" + systemd-dissect -U "$imgs/mnt" +} + +testcase_free_area_calculation() { + local defs imgs output + + if ! command -v mksquashfs >/dev/null; then + echo "Skipping free area calculation test without squashfs." + 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" + + # https://github.com/systemd/systemd/issues/28225 + echo "*** free area calculation ***" + + tee "$defs/00-ESP.conf" <<EOF +[Partition] +Type = esp +Label = ESP +Format = vfat + +SizeMinBytes = 128M +SizeMaxBytes = 128M + +# Sufficient for testing +CopyFiles = /etc:/ +EOF + + tee "$defs/10-os.conf" <<EOF +[Partition] +Type = root-${architecture} +Label = test +Format = squashfs + +Minimize = best +# Sufficient for testing +CopyFiles = /etc/:/ + +VerityMatchKey = os +Verity = data +EOF + + tee "$defs/11-os-verity.conf" <<EOF +[Partition] +Type = root-${architecture}-verity +Label = test + +Minimize = best + +VerityMatchKey = os +Verity = hash +EOF + + # Set sector size for VFAT to 512 bytes because there will not be enough FAT clusters otherwise + output1=$(SYSTEMD_REPART_MKFS_OPTIONS_VFAT="-S 512" systemd-repart \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + --empty=create \ + --size=auto \ + --sector-size=4096 \ + --defer-partitions=esp \ + --json=pretty \ + "$imgs/zzz") + + # The second invocation + output2=$(SYSTEMD_REPART_MKFS_OPTIONS_VFAT="-S 512" systemd-repart \ + --definitions="$defs" \ + --seed="$seed" \ + --dry-run=no \ + --empty=allow \ + --size=auto \ + --sector-size=4096 \ + --defer-partitions=esp \ + --json=pretty \ + "$imgs/zzz") + + diff -u <(echo "$output1" | grep -E "(offset|raw_size|raw_padding)") <(echo "$output2" | grep -E "(offset|raw_size|raw_padding)") +} + +test_sector() { + local defs imgs output loop + local start size ratio + local sector="${1?}" + + if systemd-detect-virt --quiet --container; then + echo "Skipping sector size tests in container." + return + fi + + echo "*** sector sizes ***" + + 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 + + tee "$defs/a.conf" <<EOF +[Partition] +Type=root +SizeMaxBytes=15M +SizeMinBytes=15M +EOF + tee "$defs/b.conf" <<EOF +[Partition] +Type=linux-generic +Weight=250 +EOF + + tee "$defs/c.conf" <<EOF +[Partition] +Type=linux-generic +Weight=750 +EOF + + truncate -s 100m "$imgs/$sector.img" + loop=$(losetup -b "$sector" -P --show -f "$imgs/$sector.img" ) + udevadm wait --timeout 60 --settle "${loop:?}" + + systemd-repart --offline="$OFFLINE" \ + --pretty=yes \ + --definitions="$defs" \ + --seed="$seed" \ + --empty=require \ + --dry-run=no \ + "$loop" + + sfdisk --verify "$loop" + output=$(sfdisk --dump "$loop") + losetup -d "$loop" + + ratio=$(( sector / 512 )) + start=$(( 2048 / ratio )) + size=$(( 30720 / ratio )) + assert_in "${loop}p1 : start= *${start}, size= *${size}, type=${root_guid}, uuid=${root_uuid}, name=\"root-${architecture}\", attrs=\"GUID:59\"" "$output" + start=$(( start + size )) + size=$(( 42992 / ratio )) + assert_in "${loop}p2 : start= *${start}, size= *${size}, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=DF71F5E3-080A-4D16-824B-18591B881380, name=\"linux-generic\"" "$output" + start=$(( start + size )) + size=$(( 129000 / ratio )) + assert_in "${loop}p3 : start= *${start}, size= *${size}, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=DB081670-07AE-48CA-9F5E-813D5E40B976, name=\"linux-generic-2\"" "$output" +} + +testcase_dropped_partitions() { + local workdir image defs + + workdir="$(mktemp --directory "/tmp/test-repart.dropped-partitions.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '${workdir:?}'" RETURN + + image="$workdir/image.img" + truncate -s 32M "$image" + + defs="$workdir/defs" + mkdir "$defs" + echo -ne "[Partition]\nType=root\n" >"$defs/10-part1.conf" + echo -ne "[Partition]\nType=root\nSizeMinBytes=1T\nPriority=1\n" >"$defs/11-dropped-first.conf" + echo -ne "[Partition]\nType=root\n" >"$defs/12-part2.conf" + echo -ne "[Partition]\nType=root\nSizeMinBytes=1T\nPriority=2\n" >"$defs/13-dropped-second.conf" + + systemd-repart --empty=allow --pretty=yes --dry-run=no --definitions="$defs" "$image" + + sfdisk -q -l "$image" + [[ "$(sfdisk -q -l "$image" | grep -c "$image")" -eq 2 ]] +} + +OFFLINE="yes" +run_testcases + +# Online image builds need loop devices so we can't run them in nspawn. +if ! systemd-detect-virt --container; then + OFFLINE="no" + run_testcases +fi + +# Valid block sizes on the Linux block layer are >= 512 and <= PAGE_SIZE, and +# must be powers of 2. Which leaves exactly four different ones to test on +# typical hardware +test_sector 512 +test_sector 1024 +test_sector 2048 +test_sector 4096 + +touch /testok diff --git a/test/units/testsuite-59.service b/test/units/testsuite-59.service new file mode 100644 index 0000000..f85cfab --- /dev/null +++ b/test/units/testsuite-59.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-59-RELOADING-RESTART + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh diff --git a/test/units/testsuite-59.sh b/test/units/testsuite-59.sh new file mode 100755 index 0000000..1b622b3 --- /dev/null +++ b/test/units/testsuite-59.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +fail() { + systemd-analyze log-level info + exit 1 +} + +# Wait for a service to enter a state within a timeout period, if it doesn't +# enter the desired state within the timeout period then this function will +# exit the test case with a non zero exit code. +wait_on_state_or_fail() { + service=$1 + expected_state=$2 + timeout=$3 + + state=$(systemctl show "$service" --property=ActiveState --value) + while [ "$state" != "$expected_state" ]; do + if [ "$timeout" = "0" ]; then + fail + fi + timeout=$((timeout - 1)) + sleep 1 + state=$(systemctl show "$service" --property=ActiveState --value) + done +} + +systemd-analyze log-level debug + + +cat >/run/systemd/system/testservice-fail-59.service <<EOF +[Unit] +Description=TEST-59-RELOADING-RESTART Normal exit + +[Service] +Type=notify +ExecStart=/bin/bash -c "systemd-notify --ready; systemd-notify RELOADING=1; sleep 1; exit 1" +EOF + +cat >/run/systemd/system/testservice-fail-restart-59.service <<EOF +[Unit] +Description=TEST-59-RELOADING-RESTART Restart=on-failure + +[Service] +Type=notify +ExecStart=/bin/bash -c "systemd-notify --ready; systemd-notify RELOADING=1; sleep 1; exit 1" +Restart=on-failure +StartLimitBurst=1 +EOF + + +cat >/run/systemd/system/testservice-abort-restart-59.service <<EOF +[Unit] +Description=TEST-59-RELOADING-RESTART Restart=on-abort + +[Service] +Type=notify +ExecStart=/bin/bash -c "systemd-notify --ready; systemd-notify RELOADING=1; sleep 5; exit 1" +Restart=on-abort +EOF + +systemctl daemon-reload + +# This service sends a RELOADING=1 message then exits before it sends a +# READY=1. Ensure it enters failed state and does not linger in reloading +# state. +systemctl start testservice-fail-59.service +wait_on_state_or_fail "testservice-fail-59.service" "failed" "30" + +# This service sends a RELOADING=1 message then exits before it sends a +# READY=1. It should automatically restart on failure. Ensure it enters failed +# state and does not linger in reloading state. +systemctl start testservice-fail-restart-59.service +wait_on_state_or_fail "testservice-fail-restart-59.service" "failed" "30" + +# This service sends a RELOADING=1 message then exits before it sends a +# READY=1. It should automatically restart on abort. It will sleep for 5s +# to allow us to send it a SIGABRT. Ensure the service enters the failed state +# and does not linger in reloading state. +systemctl start testservice-abort-restart-59.service +systemctl --signal=SIGABRT kill testservice-abort-restart-59.service +wait_on_state_or_fail "testservice-abort-restart-59.service" "failed" "30" + +systemd-analyze log-level info + +# Test that rate-limiting daemon-reload works +mkdir -p /run/systemd/system.conf.d/ +cat >/run/systemd/system.conf.d/50-test-59-reload.conf <<EOF +[Manager] +ReloadLimitIntervalSec=9 +ReloadLimitBurst=3 +EOF + +# Pick up the new config +systemctl daemon-reload + +# The timeout will hit (and the test will fail) if the reloads are not rate-limited +timeout 15 bash -c 'while systemctl daemon-reload --no-block; do true; done' + +# Rate limit should reset after 9s +sleep 10 + +systemctl daemon-reload + +# Let's now test the notify-reload logic + +cat >/run/notify-reload-test.sh <<EOF +#!/usr/bin/env bash +set -eux +set -o pipefail + +EXIT_STATUS=88 +LEAVE=0 + +function reload() { + systemd-notify --reloading --status="Adding 11 to exit status" + EXIT_STATUS=\$((EXIT_STATUS + 11)) + systemd-notify --ready --status="Back running" +} + +function leave() { + systemd-notify --stopping --status="Adding 7 to exit status" + EXIT_STATUS=\$((EXIT_STATUS + 7)) + LEAVE=1 + return 0 +} + +trap reload SIGHUP +trap leave SIGTERM + +systemd-notify --ready +systemd-notify --status="Running now" + +while [ \$LEAVE = 0 ] ; do + sleep 1 +done + +systemd-notify --status="Adding 3 to exit status" +EXIT_STATUS=\$((EXIT_STATUS + 3)) +exit \$EXIT_STATUS +EOF + +chmod +x /run/notify-reload-test.sh + +systemd-analyze log-level debug + +systemd-run --unit notify-reload-test -p Type=notify-reload -p KillMode=process /run/notify-reload-test.sh +systemctl reload notify-reload-test +systemctl stop notify-reload-test + +test "$(systemctl show -p ExecMainStatus --value notify-reload-test)" = 109 + +systemctl reset-failed notify-reload-test +rm /run/notify-reload-test.sh + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-60.service b/test/units/testsuite-60.service new file mode 100644 index 0000000..1a929e4 --- /dev/null +++ b/test/units/testsuite-60.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-60-MOUNT-RATELIMIT + +[Service] +Type=oneshot +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh diff --git a/test/units/testsuite-60.sh b/test/units/testsuite-60.sh new file mode 100755 index 0000000..e800a7a --- /dev/null +++ b/test/units/testsuite-60.sh @@ -0,0 +1,308 @@ +#!/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 + +teardown_test_dependencies() ( + set +eux + + if mountpoint /tmp/deptest; then + umount /tmp/deptest + fi + + if [[ -n "${LOOP}" ]]; then + losetup -d "${LOOP}" || : + fi + if [[ -n "${LOOP_0}" ]]; then + losetup -d "${LOOP_0}" || : + fi + if [[ -n "${LOOP_1}" ]]; then + losetup -d "${LOOP_1}" || : + fi + + rm -f /tmp/testsuite-60-dependencies-0.img + rm -f /tmp/testsuite-60-dependencies-1.img + + rm -f /run/systemd/system/tmp-deptest.mount + systemctl daemon-reload + + return 0 +) + +setup_loop() { + truncate -s 30m "/tmp/testsuite-60-dependencies-${1?}.img" + sfdisk --wipe=always "/tmp/testsuite-60-dependencies-${1?}.img" <<EOF +label:gpt + +name="loop${1?}-part1" +EOF + LOOP=$(losetup -P --show -f "/tmp/testsuite-60-dependencies-${1?}.img") + udevadm wait --settle --timeout=10 "${LOOP}" + udevadm lock --device="${LOOP}" mkfs.ext4 -L "partname${1?}-1" "${LOOP}p1" +} + +check_dependencies() { + local escaped_0 escaped_1 after + + escaped_0=$(systemd-escape -p "${LOOP_0}p1") + escaped_1=$(systemd-escape -p "${LOOP_1}p1") + + if [[ -f /run/systemd/system/tmp-deptest.mount ]]; then + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_not_in "local-fs-pre.target" "$after" + assert_in "remote-fs-pre.target" "$after" + assert_in "network.target" "$after" + fi + + # mount LOOP_0 + mount -t ext4 "${LOOP_0}p1" /tmp/deptest + sleep 1 + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_in "local-fs-pre.target" "$after" + assert_not_in "remote-fs-pre.target" "$after" + assert_not_in "network.target" "$after" + assert_in "${escaped_0}.device" "$after" + assert_in "blockdev@${escaped_0}.target" "$after" + assert_not_in "${escaped_1}.device" "$after" + assert_not_in "blockdev@${escaped_1}.target" "$after" + umount /tmp/deptest + + if [[ -f /run/systemd/system/tmp-deptest.mount ]]; then + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_not_in "local-fs-pre.target" "$after" + assert_in "remote-fs-pre.target" "$after" + assert_in "network.target" "$after" + fi + + # mount LOOP_1 (using fake _netdev option) + mount -t ext4 -o _netdev "${LOOP_1}p1" /tmp/deptest + sleep 1 + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_not_in "local-fs-pre.target" "$after" + assert_in "remote-fs-pre.target" "$after" + assert_in "network.target" "$after" + assert_not_in "${escaped_0}.device" "$after" + assert_not_in "blockdev@${escaped_0}.target" "$after" + assert_in "${escaped_1}.device" "$after" + assert_in "blockdev@${escaped_1}.target" "$after" + umount /tmp/deptest + + if [[ -f /run/systemd/system/tmp-deptest.mount ]]; then + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_not_in "local-fs-pre.target" "$after" + assert_in "remote-fs-pre.target" "$after" + assert_in "network.target" "$after" + fi + + # mount tmpfs + mount -t tmpfs tmpfs /tmp/deptest + sleep 1 + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_in "local-fs-pre.target" "$after" + assert_not_in "remote-fs-pre.target" "$after" + assert_not_in "network.target" "$after" + assert_not_in "${escaped_0}.device" "$after" + assert_not_in "blockdev@${escaped_0}.target" "$after" + assert_not_in "${escaped_1}.device" "$after" + assert_not_in "blockdev@${escaped_1}.target" "$after" + umount /tmp/deptest + + if [[ -f /run/systemd/system/tmp-deptest.mount ]]; then + after=$(systemctl show --property=After --value tmp-deptest.mount) + assert_not_in "local-fs-pre.target" "$after" + assert_in "remote-fs-pre.target" "$after" + assert_in "network.target" "$after" + fi +} + +test_dependencies() { + if systemd-detect-virt --quiet --container; then + echo "Skipping test_dependencies in container" + return + fi + + trap teardown_test_dependencies RETURN + + setup_loop 0 + LOOP_0="${LOOP}" + LOOP= + setup_loop 1 + LOOP_1="${LOOP}" + LOOP= + + mkdir -p /tmp/deptest + + # without .mount file + check_dependencies + + # create .mount file + mkdir -p /run/systemd/system + cat >/run/systemd/system/tmp-deptest.mount <<EOF +[Mount] +Where=/tmp/deptest +What=192.168.0.1:/tmp/mnt +Type=nfs +EOF + systemctl daemon-reload + + # with .mount file + check_dependencies +} + +test_issue_20329() { + local tmpdir unit + tmpdir="$(mktemp -d)" + unit=$(systemd-escape --suffix mount --path "$tmpdir") + + # Set up test mount unit + cat >/run/systemd/system/"$unit" <<EOF +[Mount] +What=tmpfs +Where=$tmpdir +Type=tmpfs +Options=defaults,nofail +EOF + + # Start the unit + systemctl daemon-reload + systemctl start "$unit" + + [[ "$(systemctl show --property SubState --value "$unit")" = "mounted" ]] || { + echo >&2 "Test mount \"$unit\" unit isn't mounted" + return 1 + } + mountpoint -q "$tmpdir" + + trap 'systemctl stop $unit' RETURN + + # Trigger the mount ratelimiting + cd "$(mktemp -d)" + mkdir foo + for _ in {1..50}; do + mount --bind foo foo + umount foo + done + + # Unmount the test mount and start it immediately again via systemd + umount "$tmpdir" + systemctl start "$unit" + + # Make sure it is seen as mounted by systemd and it actually is mounted + [[ "$(systemctl show --property SubState --value "$unit")" = "mounted" ]] || { + echo >&2 "Test mount \"$unit\" unit isn't in \"mounted\" state" + return 1 + } + + mountpoint -q "$tmpdir" || { + echo >&2 "Test mount \"$unit\" is in \"mounted\" state, actually is not mounted" + return 1 + } +} + +test_issue_23796() { + local mount_path mount_mytmpfs + + mount_path="$(command -v mount 2>/dev/null)" + mount_mytmpfs="${mount_path/\/bin/\/sbin}.mytmpfs" + cat >"$mount_mytmpfs" <<EOF +#!/bin/bash +sleep ".\$RANDOM" +exec -- $mount_path -t tmpfs tmpfs "\$2" +EOF + chmod +x "$mount_mytmpfs" + + mkdir -p /run/systemd/system + cat >/run/systemd/system/tmp-hoge.mount <<EOF +[Mount] +What=mytmpfs +Where=/tmp/hoge +Type=mytmpfs +EOF + + # shellcheck disable=SC2064 + trap "rm -f /run/systemd/system/tmp-hoge.mount '$mount_mytmpfs'" RETURN + + for _ in {1..10}; do + systemctl --no-block start tmp-hoge.mount + sleep ".$RANDOM" + systemctl daemon-reexec + + sleep 1 + + if [[ "$(systemctl is-failed tmp-hoge.mount)" == "failed" ]] || \ + journalctl -u tmp-hoge.mount -q --grep "but there is no mount"; then + exit 1 + fi + + systemctl stop tmp-hoge.mount + done +} + +systemd-analyze log-level debug +systemd-analyze log-target journal + +NUM_DIRS=20 + +# make sure we can handle mounts at very long paths such that mount unit name must be hashed to fall within our unit name limit +LONGPATH="$(printf "/$(printf "x%0.s" {1..255})%0.s" {1..7})" +LONGMNT="$(systemd-escape --suffix=mount --path "$LONGPATH")" +TS="$(date '+%H:%M:%S')" + +mkdir -p "$LONGPATH" +mount -t tmpfs tmpfs "$LONGPATH" +systemctl daemon-reload + +# check that unit is active(mounted) +systemctl --no-pager show -p SubState --value "$LONGPATH" | grep -q mounted + +# check that relevant part of journal doesn't contain any errors related to unit +[ "$(journalctl -b --since="$TS" --priority=err | grep -c "$LONGMNT")" = "0" ] + +# check that we can successfully stop the mount unit +systemctl stop "$LONGPATH" +rm -rf "$LONGPATH" + +# mount/unmount enough times to trigger the /proc/self/mountinfo parsing rate limiting + +for ((i = 0; i < NUM_DIRS; i++)); do + mkdir "/tmp/meow${i}" +done + +TS="$(date '+%H:%M:%S')" + +for ((i = 0; i < NUM_DIRS; i++)); do + mount -t tmpfs tmpfs "/tmp/meow${i}" +done + +systemctl daemon-reload +systemctl list-units -t mount tmp-meow* | grep -q tmp-meow + +for ((i = 0; i < NUM_DIRS; i++)); do + umount "/tmp/meow${i}" +done + +# Figure out if we have entered the rate limit state. +# If the infra is slow we might not enter the rate limit state; in that case skip the exit check. +if timeout 2m bash -c "until journalctl -u init.scope --since=$TS | grep -q '(mount-monitor-dispatch) entered rate limit'; do sleep 1; done"; then + timeout 2m bash -c "until journalctl -u init.scope --since=$TS | grep -q '(mount-monitor-dispatch) left rate limit'; do sleep 1; done" +fi + +# Verify that the mount units are always cleaned up at the end. +# Give some time for units to settle so we don't race between exiting the rate limit state and cleaning up the units. +timeout 2m bash -c 'while systemctl list-units -t mount tmp-meow* | grep -q tmp-meow; do systemctl daemon-reload; sleep 10; done' + +# test for issue #19983 and #23552. +test_dependencies + +# test that handling of mount start jobs is delayed when /proc/self/mouninfo monitor is rate limited +test_issue_20329 + +# test for reexecuting with background mount job +test_issue_23796 + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-62-1.service b/test/units/testsuite-62-1.service new file mode 100644 index 0000000..fa3a7e7 --- /dev/null +++ b/test/units/testsuite-62-1.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-62-RESTRICT-IFACES-all-pings-work +[Service] +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.1' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.9' +RestrictNetworkInterfaces= +Type=oneshot diff --git a/test/units/testsuite-62-2.service b/test/units/testsuite-62-2.service new file mode 100644 index 0000000..b83362d --- /dev/null +++ b/test/units/testsuite-62-2.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-62-RESTRICT-IFACES-allow-list +[Service] +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.1' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5' +ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.9' +RestrictNetworkInterfaces=veth0 +RestrictNetworkInterfaces=veth1 +Type=oneshot diff --git a/test/units/testsuite-62-3.service b/test/units/testsuite-62-3.service new file mode 100644 index 0000000..b6c8e7a --- /dev/null +++ b/test/units/testsuite-62-3.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-62-RESTRICT-IFACES-deny-list +[Service] +ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.1' +ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.5' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.9' +RestrictNetworkInterfaces=~veth0 +RestrictNetworkInterfaces=~veth1 +Type=oneshot diff --git a/test/units/testsuite-62-4.service b/test/units/testsuite-62-4.service new file mode 100644 index 0000000..053e6d2 --- /dev/null +++ b/test/units/testsuite-62-4.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-62-RESTRICT-IFACES-empty-assignment +[Service] +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.1' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.9' +RestrictNetworkInterfaces=veth0 +RestrictNetworkInterfaces= +Type=oneshot diff --git a/test/units/testsuite-62-5.service b/test/units/testsuite-62-5.service new file mode 100644 index 0000000..a8f268d --- /dev/null +++ b/test/units/testsuite-62-5.service @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-62-RESTRICT-IFACES-invert-assignment +[Service] +ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.1' +ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5' +ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.9' +RestrictNetworkInterfaces=veth0 +RestrictNetworkInterfaces=veth0 veth1 +RestrictNetworkInterfaces=~veth0 +Type=oneshot diff --git a/test/units/testsuite-62.service b/test/units/testsuite-62.service new file mode 100644 index 0000000..5c3f94d --- /dev/null +++ b/test/units/testsuite-62.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-62-RESTRICT-IFACES + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-62.sh b/test/units/testsuite-62.sh new file mode 100755 index 0000000..ed40821 --- /dev/null +++ b/test/units/testsuite-62.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +setup() { + systemd-analyze log-level debug + + for i in {0..3}; + do + ip netns del "ns${i}" || true + ip link del "veth${i}" || true + ip netns add "ns${i}" + ip link add "veth${i}" type veth peer name "veth${i}_" + ip link set "veth${i}_" netns "ns${i}" + ip -n "ns${i}" link set dev "veth${i}_" up + ip -n "ns${i}" link set dev lo up + ip -n "ns${i}" addr add "192.168.113."$((4*i+1))/30 dev "veth${i}_" + ip link set dev "veth${i}" up + ip addr add "192.168.113."$((4*i+2))/30 dev "veth${i}" + done +} + +# shellcheck disable=SC2317 +teardown() { + set +e + + for i in {0..3}; do + ip netns del "ns${i}" + ip link del "veth${i}" + done + + systemd-analyze log-level info +} + +KERNEL_VERSION="$(uname -r)" +KERNEL_MAJOR="${KERNEL_VERSION%%.*}" +KERNEL_MINOR="${KERNEL_VERSION#"$KERNEL_MAJOR".}" +KERNEL_MINOR="${KERNEL_MINOR%%.*}" + +MAJOR_REQUIRED=5 +MINOR_REQUIRED=7 + +if [[ "$KERNEL_MAJOR" -lt $MAJOR_REQUIRED || ("$KERNEL_MAJOR" -eq $MAJOR_REQUIRED && "$KERNEL_MINOR" -lt $MINOR_REQUIRED) ]]; then + echo "kernel is not 5.7+" >>/skipped + exit 0 +fi + +if systemctl --version | grep -q -F -- "-BPF_FRAMEWORK"; then + echo "bpf-framework is disabled" >>/skipped + exit 0 +fi + +trap teardown EXIT +setup + +systemctl start --wait testsuite-62-1.service +systemctl start --wait testsuite-62-2.service +systemctl start --wait testsuite-62-3.service +systemctl start --wait testsuite-62-4.service +systemctl start --wait testsuite-62-5.service + +touch /testok diff --git a/test/units/testsuite-63.service b/test/units/testsuite-63.service new file mode 100644 index 0000000..483c6a8 --- /dev/null +++ b/test/units/testsuite-63.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-63-PATH + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-63.sh b/test/units/testsuite-63.sh new file mode 100755 index 0000000..ea8cd94 --- /dev/null +++ b/test/units/testsuite-63.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +systemctl log-level debug + +# Test that a path unit continuously triggering a service that fails condition checks eventually fails with +# the trigger-limit-hit error. +rm -f /tmp/nonexistent +systemctl start test63.path +touch /tmp/test63 + +# Make sure systemd has sufficient time to hit the trigger limit for test63.path. +# shellcheck disable=SC2016 +timeout 30 bash -c 'until test "$(systemctl show test63.path -P ActiveState)" = failed; do sleep .2; done' +test "$(systemctl show test63.service -P ActiveState)" = inactive +test "$(systemctl show test63.service -P Result)" = success +test "$(systemctl show test63.path -P Result)" = trigger-limit-hit + +# Test that starting the service manually doesn't affect the path unit. +rm -f /tmp/test63 +systemctl reset-failed +systemctl start test63.path +systemctl start test63.service +test "$(systemctl show test63.service -P ActiveState)" = inactive +test "$(systemctl show test63.service -P Result)" = success +test "$(systemctl show test63.path -P ActiveState)" = active +test "$(systemctl show test63.path -P Result)" = success + +# Test that glob matching works too, with $TRIGGER_PATH +systemctl start test63-glob.path +touch /tmp/test63-glob-foo +timeout 60 bash -c 'until systemctl -q is-active test63-glob.service; do sleep .2; done' +test "$(systemctl show test63-glob.service -P ActiveState)" = active +test "$(systemctl show test63-glob.service -P Result)" = success + +test "$(busctl --json=short get-property org.freedesktop.systemd1 /org/freedesktop/systemd1/unit/test63_2dglob_2eservice org.freedesktop.systemd1.Unit ActivationDetails)" = '{"type":"a(ss)","data":[["trigger_unit","test63-glob.path"],["trigger_path","/tmp/test63-glob-foo"]]}' + +systemctl stop test63-glob.path test63-glob.service + +test "$(busctl --json=short get-property org.freedesktop.systemd1 /org/freedesktop/systemd1/unit/test63_2dglob_2eservice org.freedesktop.systemd1.Unit ActivationDetails)" = '{"type":"a(ss)","data":[]}' + +# tests for issue https://github.com/systemd/systemd/issues/24577#issuecomment-1522628906 +rm -f /tmp/hoge +systemctl start test63-issue-24577.path +systemctl status -n 0 test63-issue-24577.path +systemctl status -n 0 test63-issue-24577.service || : +systemctl list-jobs +output=$(systemctl list-jobs --no-legend) +assert_not_in "test63-issue-24577.service" "$output" +assert_not_in "test63-issue-24577-dep.service" "$output" + +touch /tmp/hoge +systemctl status -n 0 test63-issue-24577.path +systemctl status -n 0 test63-issue-24577.service || : +systemctl list-jobs +output=$(systemctl list-jobs --no-legend) +assert_in "test63-issue-24577.service" "$output" +assert_in "test63-issue-24577-dep.service" "$output" + +# even if the service is stopped, it will be soon retriggered. +systemctl stop test63-issue-24577.service +systemctl status -n 0 test63-issue-24577.path +systemctl status -n 0 test63-issue-24577.service || : +systemctl list-jobs +output=$(systemctl list-jobs --no-legend) +assert_in "test63-issue-24577.service" "$output" +assert_in "test63-issue-24577-dep.service" "$output" + +rm -f /tmp/hoge +systemctl stop test63-issue-24577.service +systemctl status -n 0 test63-issue-24577.path +systemctl status -n 0 test63-issue-24577.service || : +systemctl list-jobs +output=$(systemctl list-jobs --no-legend) +assert_not_in "test63-issue-24577.service" "$output" +assert_in "test63-issue-24577-dep.service" "$output" + +# Test for race condition fixed by https://github.com/systemd/systemd/pull/30768 +# Here's the schedule of events that we to happen during this test: +# (This test) (The service) +# .path unit monitors /tmp/copyme for changes +# Take lock on /tmp/noexeit ↓ +# Write to /tmp/copyme ↓ +# Wait for deactivating Started +# ↓ Copies /tmp/copyme to /tmp/copied +# ↓ Tells manager it's shutting down +# Ensure service did the copy Tries to lock /tmp/noexit and blocks +# Write to /tmp/copyme ↓ +# +# Now at this point the test can diverge. If we regress, this second write is +# missed and we'll see: +# ... (second write) ... (blocked) +# Drop lock on /tmp/noexit ↓ +# Wait for service to do copy Unblocks and exits +# ↓ (dead) +# ↓ +# (timeout) +# Test fails +# +# Otherwise, we'll see: +# ... (second write) ... (blocked) +# Drop lock on /tmp/noexit ↓ and .path unit queues a new start job +# Wait for service to do copy Unblocks and exits +# ↓ Starts again b/c of queued job +# ↓ Copies again +# Test Passes +systemctl start test63-pr-30768.path +exec {lock}<>/tmp/noexit +flock -e $lock +echo test1 > /tmp/copyme +# shellcheck disable=SC2016 +timeout 30 bash -c 'until test "$(systemctl show test63-pr-30768.service -P ActiveState)" = deactivating; do sleep .2; done' +diff /tmp/copyme /tmp/copied +echo test2 > /tmp/copyme +exec {lock}<&- +timeout 30 bash -c 'until diff /tmp/copyme /tmp/copied; do sleep .2; done' + +systemctl log-level info + +touch /testok diff --git a/test/units/testsuite-64.service b/test/units/testsuite-64.service new file mode 100644 index 0000000..f75a3d7 --- /dev/null +++ b/test/units/testsuite-64.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-64-UDEV + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-64.sh b/test/units/testsuite-64.sh new file mode 100755 index 0000000..65e5f6c --- /dev/null +++ b/test/units/testsuite-64.sh @@ -0,0 +1,1192 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# vi: ts=4 sw=4 tw=0 et: + +set -eux +set -o pipefail + +# Check if all symlinks under /dev/disk/ are valid +# shellcheck disable=SC2120 +helper_check_device_symlinks() {( + set +x + + local dev link path paths target + + [[ $# -gt 0 ]] && paths=("$@") || paths=("/dev/disk" "/dev/mapper") + + # Check if all given paths are valid + for path in "${paths[@]}"; do + if ! test -e "$path"; then + echo >&2 "Path '$path' doesn't exist" + return 1 + fi + done + + while read -r link; do + target="$(readlink -f "$link")" + # Both checks should do virtually the same thing, but check both to be + # on the safe side + if [[ ! -e "$link" || ! -e "$target" ]]; then + echo >&2 "ERROR: symlink '$link' points to '$target' which doesn't exist" + return 1 + fi + + # Check if the symlink points to the correct device in /dev + dev="/dev/$(udevadm info -q name "$link")" + if [[ "$target" != "$dev" ]]; then + echo >&2 "ERROR: symlink '$link' points to '$target' but '$dev' was expected" + return 1 + fi + done < <(find "${paths[@]}" -type l) +)} + +helper_check_udev_watch() {( + set +x + + local link target id dev + + while read -r link; do + target="$(readlink "$link")" + if [[ ! -L "/run/udev/watch/$target" ]]; then + echo >&2 "ERROR: symlink /run/udev/watch/$target does not exist" + return 1 + fi + if [[ "$(readlink "/run/udev/watch/$target")" != "$(basename "$link")" ]]; then + echo >&2 "ERROR: symlink target of /run/udev/watch/$target is inconsistent with $link" + return 1 + fi + + if [[ "$target" =~ ^[0-9]+$ ]]; then + # $link is ID -> wd + id="$(basename "$link")" + else + # $link is wd -> ID + id="$target" + fi + + if [[ "${id:0:1}" == "b" ]]; then + dev="/dev/block/${id:1}" + elif [[ "${id:0:1}" == "c" ]]; then + dev="/dev/char/${id:1}" + else + echo >&2 "ERROR: unexpected device ID '$id'" + return 1 + fi + + if [[ ! -e "$dev" ]]; then + echo >&2 "ERROR: device '$dev' corresponding to symlink '$link' does not exist" + return 1 + fi + done < <(find /run/udev/watch -type l) +)} + +check_device_unit() {( + set +x + + local log_level link links path syspath unit + + log_level="${1?}" + path="${2?}" + unit=$(systemd-escape --path --suffix=device "$path") + + [[ "$log_level" == 1 ]] && echo "INFO: check_device_unit($unit)" + + syspath=$(systemctl show --value --property SysFSPath "$unit" 2>/dev/null) + if [[ -z "$syspath" ]]; then + [[ "$log_level" == 1 ]] && echo >&2 "ERROR: $unit not found." + return 1 + fi + + if [[ ! -L "$path" ]]; then + if [[ ! -d "$syspath" ]]; then + [[ "$log_level" == 1 ]] && echo >&2 "ERROR: $unit exists for $syspath but it does not exist." + return 1 + fi + return 0 + fi + + if [[ ! -b "$path" && ! -c "$path" ]]; then + [[ "$log_level" == 1 ]] && echo >&2 "ERROR: invalid file type $path" + return 1 + fi + + read -r -a links < <(udevadm info -q symlink "$syspath" 2>/dev/null) + for link in "${links[@]}"; do + if [[ "/dev/$link" == "$path" ]]; then # DEVLINKS= given by -q symlink are relative to /dev + return 0 + fi + done + + read -r -a links < <(udevadm info "$syspath" | sed -ne '/SYSTEMD_ALIAS=/ { s/^E: SYSTEMD_ALIAS=//; p }' 2>/dev/null) + for link in "${links[@]}"; do + if [[ "$link" == "$path" ]]; then # SYSTEMD_ALIAS= are absolute + return 0 + fi + done + + [[ "$log_level" == 1 ]] && echo >&2 "ERROR: $unit exists for $syspath but it does not have the corresponding DEVLINKS or SYSTEMD_ALIAS." + return 1 +)} + +check_device_units() {( + set +x + + local log_level path paths + + log_level="${1?}" + shift + paths=("$@") + + for path in "${paths[@]}"; do + if ! check_device_unit "$log_level" "$path"; then + return 1 + fi + done + + while read -r unit _; do + path=$(systemd-escape --path --unescape "$unit") + if ! check_device_unit "$log_level" "$path"; then + return 1 + fi + done < <(systemctl list-units --all --type=device --no-legend dev-* | awk '$1 !~ /dev-tty.+/ { print $1 }' | sed -e 's/\.device$//') + + return 0 +)} + +helper_check_device_units() {( + set +x + + local i + + for i in {1..20}; do + (( i > 1 )) && sleep 0.5 + if check_device_units 0 "$@"; then + return 0 + fi + done + + check_device_units 1 "$@" +)} + +testcase_megasas2_basic() { + lsblk -S + [[ "$(lsblk --scsi --noheadings | wc -l)" -ge 128 ]] +} + +testcase_nvme_basic() { + local expected_symlinks=() + local i + + for (( i = 0; i < 5; i++ )); do + expected_symlinks+=( + # both replace mode provides the same devlink + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef"$i" + # with nsid + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef"$i"_1 + ) + done + for (( i = 5; i < 10; i++ )); do + expected_symlinks+=( + # old replace mode + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl__deadbeef_"$i" + # newer replace mode + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____deadbeef__"$i" + # with nsid + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____deadbeef__"$i"_1 + ) + done + for (( i = 10; i < 15; i++ )); do + expected_symlinks+=( + # old replace mode does not provide devlink, as serial contains "/" + # newer replace mode + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____dead_beef_"$i" + # with nsid + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____dead_beef_"$i"_1 + ) + done + for (( i = 15; i < 20; i++ )); do + expected_symlinks+=( + # old replace mode does not provide devlink, as serial contains "/" + # newer replace mode + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_dead_.._.._beef_"$i" + # with nsid + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_dead_.._.._beef_"$i"_1 + ) + done + + udevadm settle + ls /dev/disk/by-id + for i in "${expected_symlinks[@]}"; do + udevadm wait --settle --timeout=30 "$i" + done + + lsblk --noheadings | grep "^nvme" + [[ "$(lsblk --noheadings | grep -c "^nvme")" -ge 20 ]] +} + +testcase_nvme_subsystem() { + local expected_symlinks=( + # Controller(s) + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef_16 + /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef_17 + # Shared namespaces + /dev/disk/by-path/pci-*-nvme-16 + /dev/disk/by-path/pci-*-nvme-17 + ) + + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" +} + +testcase_virtio_scsi_identically_named_partitions() { + local num + + if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then + num=$((4 * 4)) + else + num=$((16 * 8)) + fi + + lsblk --noheadings -a -o NAME,PARTLABEL + [[ "$(lsblk --noheadings -a -o NAME,PARTLABEL | grep -c "Hello world")" -eq "$num" ]] +} + +testcase_multipath_basic_failover() { + local dmpath i path wwid + + # Configure multipath + cat >/etc/multipath.conf <<\EOF +defaults { + # Use /dev/mapper/$WWN paths instead of /dev/mapper/mpathX + user_friendly_names no + find_multipaths yes + enable_foreign "^$" +} + +blacklist_exceptions { + property "(SCSI_IDENT_|ID_WWN)" +} + +blacklist { +} +EOF + modprobe -v dm_multipath + systemctl start multipathd.service + systemctl status multipathd.service + multipath -ll + udevadm settle + ls -l /dev/disk/by-id/ + + for i in {0..15}; do + wwid="deaddeadbeef$(printf "%.4d" "$i")" + path="/dev/disk/by-id/wwn-0x$wwid" + dmpath="$(readlink -f "$path")" + + lsblk "$path" + multipath -C "$dmpath" + # We should have 4 active paths for each multipath device + [[ "$(multipath -l "$path" | grep -c running)" -eq 4 ]] + done + + # Test failover (with the first multipath device that has a partitioned disk) + echo "${FUNCNAME[0]}: test failover" + local device expected link mpoint part + local -a devices + mkdir -p /mnt + mpoint="$(mktemp -d /mnt/mpathXXX)" + wwid="deaddeadbeef0000" + path="/dev/disk/by-id/wwn-0x$wwid" + + # All following symlinks should exists and should be valid + local -a part_links=( + "/dev/disk/by-id/wwn-0x$wwid-part2" + "/dev/disk/by-partlabel/failover_part" + "/dev/disk/by-partuuid/deadbeef-dead-dead-beef-000000000000" + "/dev/disk/by-label/failover_vol" + "/dev/disk/by-uuid/deadbeef-dead-dead-beef-111111111111" + ) + udevadm wait --settle --timeout=30 "${part_links[@]}" + helper_check_device_units "${part_links[@]}" + + # Choose a random symlink to the failover data partition each time, for + # a better coverage + part="${part_links[$RANDOM % ${#part_links[@]}]}" + + # Get all devices attached to a specific multipath device (in H:C:T:L format) + # and sort them in a random order, so we cut off different paths each time + mapfile -t devices < <(multipath -l "$path" | grep -Eo '[0-9]+:[0-9]+:[0-9]+:[0-9]+' | sort -R) + if [[ "${#devices[@]}" -ne 4 ]]; then + echo "Expected 4 devices attached to WWID=$wwid, got ${#devices[@]} instead" + return 1 + fi + # Drop the last path from the array, since we want to leave at least one path active + unset "devices[3]" + # Mount the first multipath partition, write some data we can check later, + # and then disconnect the remaining paths one by one while checking if we + # can still read/write from the mount + mount -t ext4 "$part" "$mpoint" + expected=0 + echo -n "$expected" >"$mpoint/test" + # Sanity check we actually wrote what we wanted + [[ "$(<"$mpoint/test")" == "$expected" ]] + + for device in "${devices[@]}"; do + echo offline >"/sys/class/scsi_device/$device/device/state" + [[ "$(<"$mpoint/test")" == "$expected" ]] + expected="$((expected + 1))" + echo -n "$expected" >"$mpoint/test" + + # Make sure all symlinks are still valid + udevadm wait --settle --timeout=30 "${part_links[@]}" + helper_check_device_units "${part_links[@]}" + done + + multipath -l "$path" + # Three paths should be now marked as 'offline' and one as 'running' + [[ "$(multipath -l "$path" | grep -c offline)" -eq 3 ]] + [[ "$(multipath -l "$path" | grep -c running)" -eq 1 ]] + + umount "$mpoint" + rm -fr "$mpoint" +} + +testcase_simultaneous_events_1() { + local disk expected i iterations key link num_part part partscript rule target timeout + local -a devices symlinks + local -A running + + if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then + num_part=2 + iterations=10 + timeout=240 + else + num_part=10 + iterations=100 + timeout=30 + fi + + for disk in {0..9}; do + link="/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_deadbeeftest${disk}" + target="$(readlink -f "$link")" + if [[ ! -b "$target" ]]; then + echo "ERROR: failed to find the test SCSI block device $link" + return 1 + fi + + devices+=("$target") + done + + for ((part = 1; part <= num_part; part++)); do + symlinks+=( + "/dev/disk/by-partlabel/test${part}" + ) + done + + partscript="$(mktemp)" + + cat >"$partscript" <<EOF +$(for ((part = 1; part <= num_part; part++)); do printf 'name="test%d", size=2M\n' "$part"; done) +EOF + + rule=/run/udev/rules.d/50-test.rules + mkdir -p "${rule%/*}" + cat >"$rule" <<EOF +SUBSYSTEM=="block", KERNEL=="${devices[4]##*/}*|${devices[5]##*/}*", OPTIONS="link_priority=10" +EOF + + udevadm control --reload + + # initialize partition table + for disk in {0..9}; do + echo 'label: gpt' | udevadm lock --device="${devices[$disk]}" sfdisk -q "${devices[$disk]}" + done + + # Delete the partitions, immediately recreate them, wait for udev to settle + # down, and then check if we have any dangling symlinks in /dev/disk/. Rinse + # and repeat. + # + # On unpatched udev versions the delete-recreate cycle may trigger a race + # leading to dead symlinks in /dev/disk/ + for ((i = 1; i <= iterations; i++)); do + for disk in {0..9}; do + if ((disk % 2 == i % 2)); then + udevadm lock --device="${devices[$disk]}" sfdisk -q --delete "${devices[$disk]}" & + else + udevadm lock --device="${devices[$disk]}" sfdisk -q -X gpt "${devices[$disk]}" <"$partscript" & + fi + running[$disk]=$! + done + + for key in "${!running[@]}"; do + wait "${running[$key]}" + unset "running[$key]" + done + + if ((i % 10 <= 1)); then + udevadm wait --settle --timeout="$timeout" "${devices[@]}" "${symlinks[@]}" + helper_check_device_symlinks + helper_check_udev_watch + for ((part = 1; part <= num_part; part++)); do + link="/dev/disk/by-partlabel/test${part}" + target="$(readlink -f "$link")" + if ((i % 2 == 0)); then + expected="${devices[5]}$part" + else + expected="${devices[4]}$part" + fi + if [[ "$target" != "$expected" ]]; then + echo >&2 "ERROR: symlink '/dev/disk/by-partlabel/test${part}' points to '$target' but '$expected' was expected" + return 1 + fi + done + fi + done + + helper_check_device_units + rm -f "$rule" "$partscript" + + udevadm control --reload +} + +testcase_simultaneous_events_2() { + local disk expected i iterations key link num_part part script_dir target timeout + local -a devices symlinks + local -A running + + script_dir="$(mktemp --directory "/tmp/test-udev-storage.script.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$script_dir'" RETURN + + if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then + num_part=20 + iterations=1 + timeout=2400 + else + num_part=100 + iterations=3 + timeout=300 + fi + + for disk in {0..9}; do + link="/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_deadbeeftest${disk}" + target="$(readlink -f "$link")" + if [[ ! -b "$target" ]]; then + echo "ERROR: failed to find the test SCSI block device $link" + return 1 + fi + + devices+=("$target") + done + + for ((i = 1; i <= iterations; i++)); do + cat >"$script_dir/partscript-$i" <<EOF +$(for ((part = 1; part <= num_part; part++)); do printf 'name="testlabel-%d", size=1M\n' "$i"; done) +EOF + done + + echo "## $iterations iterations start: $(date '+%H:%M:%S.%N')" + for ((i = 1; i <= iterations; i++)); do + + for disk in {0..9}; do + udevadm lock --device="${devices[$disk]}" sfdisk -q --delete "${devices[$disk]}" & + running[$disk]=$! + done + + for key in "${!running[@]}"; do + wait "${running[$key]}" + unset "running[$key]" + done + + for disk in {0..9}; do + udevadm lock --device="${devices[$disk]}" sfdisk -q -X gpt "${devices[$disk]}" <"$script_dir/partscript-$i" & + running[$disk]=$! + done + + for key in "${!running[@]}"; do + wait "${running[$key]}" + unset "running[$key]" + done + + udevadm wait --settle --timeout="$timeout" "${devices[@]}" "/dev/disk/by-partlabel/testlabel-$i" + done + echo "## $iterations iterations end: $(date '+%H:%M:%S.%N')" +} + +testcase_simultaneous_events() { + testcase_simultaneous_events_1 + testcase_simultaneous_events_2 +} + +testcase_lvm_basic() { + local i iterations partitions part timeout + local vgroup="MyTestGroup$RANDOM" + local devices=( + /dev/disk/by-id/ata-foobar_deadbeeflvm{0..3} + ) + + if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then + timeout=180 + else + timeout=30 + fi + # Make sure all the necessary soon-to-be-LVM devices exist + ls -l "${devices[@]}" + + # Add all test devices into a volume group, create two logical volumes, + # and check if necessary symlinks exist (and are valid) + lvm pvcreate -y "${devices[@]}" + lvm pvs + lvm vgcreate "$vgroup" -y "${devices[@]}" + lvm vgs + lvm vgchange -ay "$vgroup" + lvm lvcreate -y -L 4M "$vgroup" -n mypart1 + lvm lvcreate -y -L 32M "$vgroup" -n mypart2 + lvm lvs + udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" + mkfs.ext4 -L mylvpart1 "/dev/$vgroup/mypart1" + udevadm wait --settle --timeout="$timeout" "/dev/disk/by-label/mylvpart1" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + + # Mount mypart1 through by-label devlink + mkdir -p /tmp/mypart1-mount-point + mount /dev/disk/by-label/mylvpart1 /tmp/mypart1-mount-point + timeout 30 bash -c "until systemctl -q is-active /tmp/mypart1-mount-point; do sleep .2; done" + # Extend the partition and check if the device and mount units are still active. + # See https://bugzilla.redhat.com/show_bug.cgi?id=2158628 + # Note, the test below may be unstable with LVM2 without the following patch: + # https://github.com/lvmteam/lvm2/pull/105 + # But, to reproduce the issue, udevd must start to process the first 'change' uevent + # earlier than extending the volume has been finished, and in most case, the extension + # is hopefully fast. + lvm lvextend -y --size 8M "/dev/$vgroup/mypart1" + udevadm wait --settle --timeout="$timeout" "/dev/disk/by-label/mylvpart1" + timeout 30 bash -c "until systemctl -q is-active '/dev/$vgroup/mypart1'; do sleep .2; done" + timeout 30 bash -c "until systemctl -q is-active /tmp/mypart1-mount-point; do sleep .2; done" + # Umount the partition, otherwise the underlying device unit will stay in + # the inactive state and not be collected, and helper_check_device_units() will fail. + systemctl show /tmp/mypart1-mount-point + umount /tmp/mypart1-mount-point + + # Rename partitions (see issue #24518) + lvm lvrename "/dev/$vgroup/mypart1" renamed1 + lvm lvrename "/dev/$vgroup/mypart2" renamed2 + udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" + udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/renamed1" "/dev/$vgroup/renamed2" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + + # Rename them back + lvm lvrename "/dev/$vgroup/renamed1" mypart1 + lvm lvrename "/dev/$vgroup/renamed2" mypart2 + udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/renamed1" "/dev/$vgroup/renamed2" + udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + + # Do not "unready" suspended encrypted devices w/o superblock info + # See: + # - https://github.com/systemd/systemd/pull/24177 + # - https://bugzilla.redhat.com/show_bug.cgi?id=1985288 + dd if=/dev/urandom of=/etc/lvm_keyfile bs=64 count=1 iflag=fullblock + chmod 0600 /etc/lvm_keyfile + # Intentionally use weaker cipher-related settings, since we don't care + # about security here as it's a throwaway LUKS partition + cryptsetup luksFormat -q --use-urandom --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ + "/dev/$vgroup/mypart2" /etc/lvm_keyfile + # Mount the LUKS partition & create a filesystem on it + mkdir -p /tmp/lvmluksmnt + cryptsetup open --key-file=/etc/lvm_keyfile "/dev/$vgroup/mypart2" "lvmluksmap" + udevadm wait --settle --timeout="$timeout" "/dev/mapper/lvmluksmap" + mkfs.ext4 -L lvmluksfs "/dev/mapper/lvmluksmap" + udevadm wait --settle --timeout="$timeout" "/dev/disk/by-label/lvmluksfs" + # Make systemd "interested" in the mount by adding it to /etc/fstab + echo "/dev/disk/by-label/lvmluksfs /tmp/lvmluksmnt ext4 defaults 0 2" >>/etc/fstab + systemctl daemon-reload + mount "/tmp/lvmluksmnt" + mountpoint "/tmp/lvmluksmnt" + # Temporarily suspend the LUKS device and trigger udev - basically what `cryptsetup resize` + # does but in a more deterministic way suitable for a test/reproducer + for _ in {0..5}; do + dmsetup suspend "/dev/mapper/lvmluksmap" + udevadm trigger -v --settle "/dev/mapper/lvmluksmap" + dmsetup resume "/dev/mapper/lvmluksmap" + # The mount should survive this sequence of events + mountpoint "/tmp/lvmluksmnt" + done + # Cleanup + umount "/tmp/lvmluksmnt" + cryptsetup close "/dev/mapper/lvmluksmap" + sed -i "/lvmluksfs/d" "/etc/fstab" + systemctl daemon-reload + + # Disable the VG and check symlinks... + lvm vgchange -an "$vgroup" + udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup" "/dev/disk/by-label/mylvpart1" + helper_check_device_symlinks "/dev/disk" + helper_check_device_units + + # reenable the VG and check the symlinks again if all LVs are properly activated + lvm vgchange -ay "$vgroup" + udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" "/dev/disk/by-label/mylvpart1" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + + # Same as above, but now with more "stress" + if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then + iterations=10 + else + iterations=50 + fi + + for ((i = 1; i <= iterations; i++)); do + lvm vgchange -an "$vgroup" + lvm vgchange -ay "$vgroup" + + if ((i % 5 == 0)); then + udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" "/dev/disk/by-label/mylvpart1" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + fi + done + + # Remove the first LV + lvm lvremove -y "$vgroup/mypart1" + udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/mypart1" + udevadm wait --timeout=0 "/dev/$vgroup/mypart2" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + + # Create & remove LVs in a loop, i.e. with more "stress" + if [[ -v ASAN_OPTIONS ]]; then + iterations=8 + partitions=16 + elif [[ "$(systemd-detect-virt -v)" == "qemu" ]]; then + iterations=8 + partitions=8 + else + iterations=16 + partitions=16 + fi + + for ((i = 1; i <= iterations; i++)); do + # 1) Create some logical volumes + for ((part = 0; part < partitions; part++)); do + lvm lvcreate -y -L 4M "$vgroup" -n "looppart$part" + done + + # 2) Immediately remove them + lvm lvremove -y $(seq -f "$vgroup/looppart%g" 0 "$((partitions - 1))") + + # 3) On every 4th iteration settle udev and check if all partitions are + # indeed gone, and if all symlinks are still valid + if ((i % 4 == 0)); then + for ((part = 0; part < partitions; part++)); do + udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/looppart$part" + done + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + fi + done +} + +testcase_btrfs_basic() { + local dev_stub i label mpoint uuid + local devices=( + /dev/disk/by-id/ata-foobar_deadbeefbtrfs{0..3} + ) + + ls -l "${devices[@]}" + + echo "Single device: default settings" + uuid="deadbeef-dead-dead-beef-000000000000" + label="btrfs_root" + udevadm lock --device="${devices[0]}" mkfs.btrfs -f -L "$label" -U "$uuid" "${devices[0]}" + udevadm wait --settle --timeout=30 "${devices[0]}" "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label" + btrfs filesystem show + helper_check_device_symlinks + helper_check_device_units + + echo "Multiple devices: using partitions, data: single, metadata: raid1" + uuid="deadbeef-dead-dead-beef-000000000001" + label="btrfs_mpart" + udevadm lock --device="${devices[0]}" sfdisk --wipe=always "${devices[0]}" <<EOF +label: gpt + +name="diskpart1", size=85M +name="diskpart2", size=85M +name="diskpart3", size=85M +name="diskpart4", size=85M +EOF + udevadm wait --settle --timeout=30 /dev/disk/by-partlabel/diskpart{1..4} + udevadm lock --device="${devices[0]}" mkfs.btrfs -f -d single -m raid1 -L "$label" -U "$uuid" /dev/disk/by-partlabel/diskpart{1..4} + udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label" + btrfs filesystem show + helper_check_device_symlinks + helper_check_device_units + wipefs -a -f "${devices[0]}" + udevadm wait --settle --timeout=30 --removed /dev/disk/by-partlabel/diskpart{1..4} + + echo "Multiple devices: using disks, data: raid10, metadata: raid10, mixed mode" + uuid="deadbeef-dead-dead-beef-000000000002" + label="btrfs_mdisk" + udevadm lock \ + --device=/dev/disk/by-id/ata-foobar_deadbeefbtrfs0 \ + --device=/dev/disk/by-id/ata-foobar_deadbeefbtrfs1 \ + --device=/dev/disk/by-id/ata-foobar_deadbeefbtrfs2 \ + --device=/dev/disk/by-id/ata-foobar_deadbeefbtrfs3 \ + mkfs.btrfs -f -M -d raid10 -m raid10 -L "$label" -U "$uuid" "${devices[@]}" + udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label" + btrfs filesystem show + helper_check_device_symlinks + helper_check_device_units + + echo "Multiple devices: using LUKS encrypted disks, data: raid1, metadata: raid1, mixed mode" + uuid="deadbeef-dead-dead-beef-000000000003" + label="btrfs_mencdisk" + mpoint="/btrfs_enc$RANDOM" + mkdir "$mpoint" + # Create a key-file + dd if=/dev/urandom of=/etc/btrfs_keyfile bs=64 count=1 iflag=fullblock + chmod 0600 /etc/btrfs_keyfile + # Encrypt each device and add it to /etc/crypttab, so it can be mounted + # automagically later + : >/etc/crypttab + for ((i = 0; i < ${#devices[@]}; i++)); do + # Intentionally use weaker cipher-related settings, since we don't care + # about security here as it's a throwaway LUKS partition + cryptsetup luksFormat -q \ + --use-urandom --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ + --uuid "deadbeef-dead-dead-beef-11111111111$i" --label "encdisk$i" "${devices[$i]}" /etc/btrfs_keyfile + udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/deadbeef-dead-dead-beef-11111111111$i" "/dev/disk/by-label/encdisk$i" + # Add the device into /etc/crypttab, reload systemd, and then activate + # the device so we can create a filesystem on it later + echo "encbtrfs$i UUID=deadbeef-dead-dead-beef-11111111111$i /etc/btrfs_keyfile luks" >>/etc/crypttab + systemctl daemon-reload + systemctl start "systemd-cryptsetup@encbtrfs$i" + done + helper_check_device_symlinks + helper_check_device_units + # Check if we have all necessary DM devices + ls -l /dev/mapper/encbtrfs{0..3} + # Create a multi-device btrfs filesystem on the LUKS devices + udevadm lock \ + --device=/dev/mapper/encbtrfs0 \ + --device=/dev/mapper/encbtrfs1 \ + --device=/dev/mapper/encbtrfs2 \ + --device=/dev/mapper/encbtrfs3 \ + mkfs.btrfs -f -M -d raid1 -m raid1 -L "$label" -U "$uuid" /dev/mapper/encbtrfs{0..3} + udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label" + btrfs filesystem show + helper_check_device_symlinks + helper_check_device_units + # Mount it and write some data to it we can compare later + mount -t btrfs /dev/mapper/encbtrfs0 "$mpoint" + echo "hello there" >"$mpoint/test" + # "Deconstruct" the btrfs device and check if we're in a sane state (symlink-wise) + umount "$mpoint" + systemctl stop systemd-cryptsetup@encbtrfs{0..3} + udevadm wait --settle --timeout=30 --removed "/dev/disk/by-uuid/$uuid" + helper_check_device_symlinks + helper_check_device_units + # Add the mount point to /etc/fstab and check if the device can be put together + # automagically. The source device is the DM name of the first LUKS device + # (from /etc/crypttab). We have to specify all LUKS devices manually, as + # registering the necessary devices is usually initrd's job (via btrfs device scan) + dev_stub="/dev/mapper/encbtrfs" + echo "/dev/mapper/encbtrfs0 $mpoint btrfs device=${dev_stub}0,device=${dev_stub}1,device=${dev_stub}2,device=${dev_stub}3 0 2" >>/etc/fstab + # Tell systemd about the new mount + systemctl daemon-reload + # Restart cryptsetup.target to trigger autounlock of partitions in /etc/crypttab + systemctl restart cryptsetup.target + # Start the corresponding mount unit and check if the btrfs device was reconstructed + # correctly + systemctl start "${mpoint##*/}.mount" + udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label" + btrfs filesystem show + helper_check_device_symlinks + helper_check_device_units + grep "hello there" "$mpoint/test" + # Cleanup + systemctl stop "${mpoint##*/}.mount" + systemctl stop systemd-cryptsetup@encbtrfs{0..3} + sed -i "/${mpoint##*/}/d" /etc/fstab + : >/etc/crypttab + rm -fr "$mpoint" + systemctl daemon-reload + udevadm settle +} + +testcase_iscsi_lvm() { + local dev i label link lun_id mpoint target_name uuid + local target_ip="127.0.0.1" + local target_port="3260" + local vgroup="iscsi_lvm$RANDOM" + local expected_symlinks=() + local devices=( + /dev/disk/by-id/ata-foobar_deadbeefiscsi{0..3} + ) + + ls -l "${devices[@]}" + + # Start the target daemon + systemctl start tgtd + systemctl status tgtd + + echo "iSCSI LUNs backed by devices" + # See RFC3721 and RFC7143 + target_name="iqn.2021-09.com.example:iscsi.test" + # Initialize a new iSCSI target <$target_name> consisting of 4 LUNs, each + # backed by a device + tgtadm --lld iscsi --op new --mode target --tid=1 --targetname "$target_name" + for ((i = 0; i < ${#devices[@]}; i++)); do + # lun-0 is reserved by iSCSI + lun_id="$((i + 1))" + tgtadm --lld iscsi --op new --mode logicalunit --tid 1 --lun "$lun_id" -b "${devices[$i]}" + tgtadm --lld iscsi --op update --mode logicalunit --tid 1 --lun "$lun_id" + expected_symlinks+=( + "/dev/disk/by-path/ip-$target_ip:$target_port-iscsi-$target_name-lun-$lun_id" + ) + done + tgtadm --lld iscsi --op bind --mode target --tid 1 -I ALL + # Configure the iSCSI initiator + iscsiadm --mode discoverydb --type sendtargets --portal "$target_ip" --discover + iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --login + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + helper_check_device_symlinks + helper_check_device_units + # Cleanup + iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --logout + tgtadm --lld iscsi --op delete --mode target --tid=1 + + echo "iSCSI LUNs backed by files + LVM" + # Note: we use files here to "trick" LVM the disks are indeed on a different + # host, so it doesn't automagically detect another path to the backing + # device once we disconnect the iSCSI devices + target_name="iqn.2021-09.com.example:iscsi.lvm.test" + mpoint="$(mktemp -d /iscsi_storeXXX)" + expected_symlinks=() + # Use the first device as it's configured with larger capacity + mkfs.ext4 -L iscsi_store "${devices[0]}" + udevadm wait --settle --timeout=30 "${devices[0]}" + mount "${devices[0]}" "$mpoint" + for i in {1..4}; do + dd if=/dev/zero of="$mpoint/lun$i.img" bs=1M count=32 + done + # Initialize a new iSCSI target <$target_name> consisting of 4 LUNs, each + # backed by a file + tgtadm --lld iscsi --op new --mode target --tid=2 --targetname "$target_name" + # lun-0 is reserved by iSCSI + for i in {1..4}; do + tgtadm --lld iscsi --op new --mode logicalunit --tid 2 --lun "$i" -b "$mpoint/lun$i.img" + tgtadm --lld iscsi --op update --mode logicalunit --tid 2 --lun "$i" + expected_symlinks+=( + "/dev/disk/by-path/ip-$target_ip:$target_port-iscsi-$target_name-lun-$i" + ) + done + tgtadm --lld iscsi --op bind --mode target --tid 2 -I ALL + # Configure the iSCSI initiator + iscsiadm --mode discoverydb --type sendtargets --portal "$target_ip" --discover + iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --login + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + helper_check_device_symlinks + helper_check_device_units + # Add all iSCSI devices into a LVM volume group, create two logical volumes, + # and check if necessary symlinks exist (and are valid) + lvm pvcreate -y "${expected_symlinks[@]}" + lvm pvs + lvm vgcreate "$vgroup" -y "${expected_symlinks[@]}" + lvm vgs + lvm vgchange -ay "$vgroup" + lvm lvcreate -y -L 4M "$vgroup" -n mypart1 + lvm lvcreate -y -L 8M "$vgroup" -n mypart2 + lvm lvs + udevadm wait --settle --timeout=30 "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" + mkfs.ext4 -L mylvpart1 "/dev/$vgroup/mypart1" + udevadm wait --settle --timeout=30 "/dev/disk/by-label/mylvpart1" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + # Disconnect the iSCSI devices and check all the symlinks + iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --logout + # "Reset" the DM state, since we yanked the backing storage from under the LVM, + # so the currently active VGs/LVs are invalid + dmsetup remove_all --deferred + # The LVM and iSCSI related symlinks should be gone + udevadm wait --settle --timeout=30 --removed "/dev/$vgroup" "/dev/disk/by-label/mylvpart1" "${expected_symlinks[@]}" + helper_check_device_symlinks "/dev/disk" + helper_check_device_units + # Reconnect the iSCSI devices and check if everything get detected correctly + iscsiadm --mode discoverydb --type sendtargets --portal "$target_ip" --discover + iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --login + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" "/dev/disk/by-label/mylvpart1" + helper_check_device_symlinks "/dev/disk" "/dev/$vgroup" + helper_check_device_units + # Cleanup + iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --logout + tgtadm --lld iscsi --op delete --mode target --tid=2 + umount "$mpoint" + rm -rf "$mpoint" +} + +testcase_long_sysfs_path() { + local cursor link logfile mpoint + local expected_symlinks=( + "/dev/disk/by-label/data_vol" + "/dev/disk/by-label/swap_vol" + "/dev/disk/by-partlabel/test_swap" + "/dev/disk/by-partlabel/test_part" + "/dev/disk/by-partuuid/deadbeef-dead-dead-beef-000000000000" + "/dev/disk/by-uuid/deadbeef-dead-dead-beef-111111111111" + "/dev/disk/by-uuid/deadbeef-dead-dead-beef-222222222222" + ) + + # Create a cursor file to skip messages generated by udevd in initrd, as it + # might not be the same up-to-date version as we currently run (hence generating + # messages we check for later and making the test fail) + cursor="$(mktemp)" + journalctl --cursor-file="${cursor:?}" -n0 -q + + # Make sure the test device is connected and show its "wonderful" path + stat /sys/block/vda + readlink -f /sys/block/vda/dev + + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + + # Try to mount the data partition manually (using its label) + mpoint="$(mktemp -d /logsysfsXXX)" + mount LABEL=data_vol "$mpoint" + touch "$mpoint/test" + umount "$mpoint" + # Do the same, but with UUID and using fstab + echo "UUID=deadbeef-dead-dead-beef-222222222222 $mpoint ext4 defaults 0 0" >>/etc/fstab + systemctl daemon-reload + mount "$mpoint" + timeout 30 bash -c "until systemctl -q is-active '$mpoint'; do sleep .2; done" + test -e "$mpoint/test" + umount "$mpoint" + + # Test out the swap partition + swapon -v -L swap_vol + swapoff -v -L swap_vol + + udevadm settle + + logfile="$(mktemp)" + # Check state of affairs after https://github.com/systemd/systemd/pull/22759 + # Note: can't use `--cursor-file` here, since we don't want to update the cursor + # after using it + [[ "$(journalctl --after-cursor="$(<"$cursor")" -q --no-pager -o short-monotonic -p info --grep "Device path.*vda.?' too long to fit into unit name" | wc -l)" -eq 0 ]] + [[ "$(journalctl --after-cursor="$(<"$cursor")" -q --no-pager -o short-monotonic --grep "Unit name .*vda.?\.device\" too long, falling back to hashed unit name" | wc -l)" -gt 0 ]] + # Check if the respective "hashed" units exist and are active (plugged) + systemctl status --no-pager "$(readlink -f /sys/block/vda/vda1)" + systemctl status --no-pager "$(readlink -f /sys/block/vda/vda2)" + # Make sure we don't unnecessarily spam the log + { journalctl -b -q --no-pager -o short-monotonic -p info --grep "/sys/devices/.+/vda[0-9]?" _PID=1 + UNIT=systemd-udevd.service || :;} | tee "$logfile" + [[ "$(wc -l <"$logfile")" -lt 10 ]] + + : >/etc/fstab + rm -fr "${cursor:?}" "${logfile:?}" "${mpoint:?}" +} + +testcase_mdadm_basic() { + local i part_name raid_name raid_dev uuid + local expected_symlinks=() + local devices=( + /dev/disk/by-id/ata-foobar_deadbeefmdadm{0..4} + ) + + ls -l "${devices[@]}" + + echo "Mirror raid (RAID 1)" + raid_name="mdmirror" + raid_dev="/dev/md/$raid_name" + part_name="${raid_name}_part" + uuid="aaaaaaaa:bbbbbbbb:cccccccc:00000001" + expected_symlinks=( + "$raid_dev" + "/dev/disk/by-id/md-name-H:$raid_name" + "/dev/disk/by-id/md-uuid-$uuid" + "/dev/disk/by-label/$part_name" # ext4 partition + ) + # Create a simple RAID 1 with an ext4 filesystem + echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/ata-foobar_deadbeefmdadm{0..1} -v -f --level=1 --raid-devices=2 + udevadm wait --settle --timeout=30 "$raid_dev" + mkfs.ext4 -L "$part_name" "$raid_dev" + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + for i in {0..9}; do + echo "Disassemble - reassemble loop, iteration #$i" + mdadm -v --stop "$raid_dev" + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + mdadm --assemble "$raid_dev" --name "$raid_name" -v + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + done + helper_check_device_symlinks + helper_check_device_units + # Cleanup + mdadm -v --stop "$raid_dev" + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + + echo "Parity raid (RAID 5)" + raid_name="mdparity" + raid_dev="/dev/md/$raid_name" + part_name="${raid_name}_part" + uuid="aaaaaaaa:bbbbbbbb:cccccccc:00000101" + expected_symlinks=( + "$raid_dev" + "/dev/disk/by-id/md-name-H:$raid_name" + "/dev/disk/by-id/md-uuid-$uuid" + "/dev/disk/by-label/$part_name" # ext4 partition + ) + # Create a simple RAID 5 with an ext4 filesystem + echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/ata-foobar_deadbeefmdadm{0..2} -v -f --level=5 --raid-devices=3 + udevadm wait --settle --timeout=30 "$raid_dev" + mkfs.ext4 -L "$part_name" "$raid_dev" + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + for i in {0..9}; do + echo "Disassemble - reassemble loop, iteration #$i" + mdadm -v --stop "$raid_dev" + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + mdadm --assemble "$raid_dev" --name "$raid_name" -v + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + done + helper_check_device_symlinks + helper_check_device_units + # Cleanup + mdadm -v --stop "$raid_dev" + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + helper_check_device_units + + echo "Mirror + parity raid (RAID 10) + multiple partitions" + raid_name="mdmirpar" + raid_dev="/dev/md/$raid_name" + part_name="${raid_name}_part" + uuid="aaaaaaaa:bbbbbbbb:cccccccc:00001010" + expected_symlinks=( + "$raid_dev" + "/dev/disk/by-id/md-name-H:$raid_name" + "/dev/disk/by-id/md-uuid-$uuid" + "/dev/disk/by-label/$part_name" # ext4 partition + # Partitions + "${raid_dev}1" + "${raid_dev}2" + "${raid_dev}3" + "/dev/disk/by-id/md-name-H:$raid_name-part1" + "/dev/disk/by-id/md-name-H:$raid_name-part2" + "/dev/disk/by-id/md-name-H:$raid_name-part3" + "/dev/disk/by-id/md-uuid-$uuid-part1" + "/dev/disk/by-id/md-uuid-$uuid-part2" + "/dev/disk/by-id/md-uuid-$uuid-part3" + ) + # Create a simple RAID 10 with an ext4 filesystem + echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/ata-foobar_deadbeefmdadm{0..3} -v -f --level=10 --raid-devices=4 + udevadm wait --settle --timeout=30 "$raid_dev" + # Partition the raid device + # Here, 'udevadm lock' is meaningless, as udevd does not lock MD devices. + sfdisk --wipe=always "$raid_dev" <<EOF +label: gpt + +uuid="deadbeef-dead-dead-beef-111111111111", name="mdpart1", size=8M +uuid="deadbeef-dead-dead-beef-222222222222", name="mdpart2", size=32M +uuid="deadbeef-dead-dead-beef-333333333333", name="mdpart3", size=16M +EOF + udevadm wait --settle --timeout=30 "/dev/disk/by-id/md-uuid-$uuid-part2" + mkfs.ext4 -L "$part_name" "/dev/disk/by-id/md-uuid-$uuid-part2" + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + for i in {0..9}; do + echo "Disassemble - reassemble loop, iteration #$i" + mdadm -v --stop "$raid_dev" + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + mdadm --assemble "$raid_dev" --name "$raid_name" -v + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + done + helper_check_device_symlinks + helper_check_device_units + # Cleanup + mdadm -v --stop "$raid_dev" + # Check if all expected symlinks were removed after the cleanup + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + helper_check_device_units +} + +testcase_mdadm_lvm() { + local part_name raid_name raid_dev uuid vgroup + local expected_symlinks=() + local devices=( + /dev/disk/by-id/ata-foobar_deadbeefmdadmlvm{0..4} + ) + + ls -l "${devices[@]}" + + raid_name="mdlvm" + raid_dev="/dev/md/$raid_name" + part_name="${raid_name}_part" + vgroup="${raid_name}_vg" + uuid="aaaaaaaa:bbbbbbbb:ffffffff:00001010" + expected_symlinks=( + "$raid_dev" + "/dev/$vgroup/mypart1" # LVM partition + "/dev/$vgroup/mypart2" # LVM partition + "/dev/disk/by-id/md-name-H:$raid_name" + "/dev/disk/by-id/md-uuid-$uuid" + "/dev/disk/by-label/$part_name" # ext4 partition + ) + # Create a RAID 10 with LVM + ext4 + echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/ata-foobar_deadbeefmdadmlvm{0..3} -v -f --level=10 --raid-devices=4 + udevadm wait --settle --timeout=30 "$raid_dev" + # Create an LVM on the MD + lvm pvcreate -y "$raid_dev" + lvm pvs + lvm vgcreate "$vgroup" -y "$raid_dev" + lvm vgs + lvm vgchange -ay "$vgroup" + lvm lvcreate -y -L 4M "$vgroup" -n mypart1 + lvm lvcreate -y -L 8M "$vgroup" -n mypart2 + lvm lvs + udevadm wait --settle --timeout=30 "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" + mkfs.ext4 -L "$part_name" "/dev/$vgroup/mypart2" + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + # Disassemble the array + lvm vgchange -an "$vgroup" + mdadm -v --stop "$raid_dev" + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + helper_check_device_symlinks + helper_check_device_units + # Reassemble it and check if all required symlinks exist + mdadm --assemble "$raid_dev" --name "$raid_name" -v + udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" + helper_check_device_symlinks + helper_check_device_units + # Cleanup + lvm vgchange -an "$vgroup" + mdadm -v --stop "$raid_dev" + # Check if all expected symlinks were removed after the cleanup + udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}" + helper_check_device_units +} + +udevadm settle +udevadm control --log-level debug +lsblk -a + +echo "Check if all symlinks under /dev/disk/ are valid (pre-test)" +helper_check_device_symlinks + +# TEST_FUNCTION_NAME is passed on the kernel command line via systemd.setenv= +# in the respective test.sh file +if ! command -v "${TEST_FUNCTION_NAME:?}"; then + echo >&2 "Missing verification handler for test case '$TEST_FUNCTION_NAME'" + exit 1 +fi + +echo "TEST_FUNCTION_NAME=$TEST_FUNCTION_NAME" +"$TEST_FUNCTION_NAME" +udevadm settle + +echo "Check if all symlinks under /dev/disk/ are valid (post-test)" +helper_check_device_symlinks + +udevadm control --log-level info + +systemctl status systemd-udevd + +touch /testok diff --git a/test/units/testsuite-65.service b/test/units/testsuite-65.service new file mode 100644 index 0000000..3610baf --- /dev/null +++ b/test/units/testsuite-65.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-65-ANALYZE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-65.sh b/test/units/testsuite-65.sh new file mode 100755 index 0000000..a6bb38d --- /dev/null +++ b/test/units/testsuite-65.sh @@ -0,0 +1,909 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +systemctl log-level debug +export SYSTEMD_LOG_LEVEL=debug + +# Sanity checks +# +# We can't really test time, critical-chain and plot verbs here, as +# the testsuite service is a part of the boot transaction, so let's assume +# they fail +systemd-analyze || : +systemd-analyze time || : +systemd-analyze critical-chain || : +# blame +systemd-analyze blame +systemd-run --wait --user --pipe -M testuser@.host systemd-analyze blame +# plot +systemd-analyze plot >/dev/null || : +systemd-analyze plot --json=pretty >/dev/null || : +systemd-analyze plot --json=short >/dev/null || : +systemd-analyze plot --json=off >/dev/null || : +systemd-analyze plot --json=pretty --no-legend >/dev/null || : +systemd-analyze plot --json=short --no-legend >/dev/null || : +systemd-analyze plot --json=off --no-legend >/dev/null || : +systemd-analyze plot --table >/dev/null || : +systemd-analyze plot --table --no-legend >/dev/null || : +# legacy/deprecated options (moved to systemctl, but still usable from analyze) +systemd-analyze log-level +systemd-analyze log-level "$(systemctl log-level)" +systemd-analyze get-log-level +systemd-analyze set-log-level "$(systemctl log-level)" +systemd-analyze log-target +systemd-analyze log-target "$(systemctl log-target)" +systemd-analyze get-log-target +systemd-analyze set-log-target "$(systemctl log-target)" +systemd-analyze service-watchdogs +systemd-analyze service-watchdogs "$(systemctl service-watchdogs)" +# dot +systemd-analyze dot >/dev/null +systemd-analyze dot systemd-journald.service >/dev/null +systemd-analyze dot systemd-journald.service systemd-logind.service >/dev/null +systemd-analyze dot --from-pattern="*" --from-pattern="*.service" systemd-journald.service >/dev/null +systemd-analyze dot --to-pattern="*" --to-pattern="*.service" systemd-journald.service >/dev/null +systemd-analyze dot --from-pattern="*.service" --to-pattern="*.service" systemd-journald.service >/dev/null +systemd-analyze dot --order systemd-journald.service systemd-logind.service >/dev/null +systemd-analyze dot --require systemd-journald.service systemd-logind.service >/dev/null +systemd-analyze dot "systemd-*.service" >/dev/null +(! systemd-analyze dot systemd-journald.service systemd-logind.service "*" bbb ccc) +# dump +# this should be rate limited to 10 calls in 10 minutes for unprivileged callers +for _ in {1..10}; do + runas testuser systemd-analyze dump systemd-journald.service >/dev/null +done +(! runas testuser systemd-analyze dump >/dev/null) +# still limited after a reload +systemctl daemon-reload +(! runas testuser systemd-analyze dump >/dev/null) +# and a re-exec +systemctl daemon-reexec +(! runas testuser systemd-analyze dump >/dev/null) +# privileged call, so should not be rate limited +for _ in {1..10}; do + systemd-analyze dump systemd-journald.service >/dev/null +done +systemd-analyze dump >/dev/null +systemd-analyze dump "*" >/dev/null +systemd-analyze dump "*.socket" >/dev/null +systemd-analyze dump "*.socket" "*.service" aaaaaaa ... >/dev/null +systemd-analyze dump systemd-journald.service >/dev/null +systemd-analyze malloc >/dev/null +(! systemd-analyze dump "") +# unit-files +systemd-analyze unit-files >/dev/null +systemd-analyze unit-files systemd-journald.service >/dev/null +systemd-analyze unit-files "*" >/dev/null +systemd-analyze unit-files "*" aaaaaa "*.service" "*.target" >/dev/null +systemd-analyze unit-files --user >/dev/null +systemd-analyze unit-files --user "*" aaaaaa "*.service" "*.target" >/dev/null +# unit-paths +systemd-analyze unit-paths +systemd-analyze unit-paths --user +systemd-analyze unit-paths --global +# exist-status +systemd-analyze exit-status +systemd-analyze exit-status STDOUT BPF +systemd-analyze exit-status 0 1 {63..65} +(! systemd-analyze exit-status STDOUT BPF "hello*") +# capability +systemd-analyze capability +systemd-analyze capability cap_chown CAP_KILL +systemd-analyze capability 0 1 {30..32} +(! systemd-analyze capability cap_chown CAP_KILL "hello*") +# condition +mkdir -p /run/systemd/system +UNIT_NAME="analyze-condition-$RANDOM.service" +cat >"/run/systemd/system/$UNIT_NAME" <<EOF +[Unit] +AssertPathExists=/etc/os-release +AssertEnvironment=!FOOBAR +ConditionKernelVersion=>1.0 +ConditionPathExists=/etc/os-release + +[Service] +ExecStart=/bin/true +EOF +systemctl daemon-reload +systemd-analyze condition --unit="$UNIT_NAME" +systemd-analyze condition 'ConditionKernelVersion = ! <4.0' \ + 'ConditionKernelVersion = >=3.1' \ + 'ConditionACPower=|false' \ + 'ConditionArchitecture=|!arm' \ + 'AssertPathExists=/etc/os-release' +(! systemd-analyze condition 'ConditionArchitecture=|!arm' 'AssertXYZ=foo') +(! systemd-analyze condition 'ConditionKernelVersion=<1.0') +(! systemd-analyze condition 'AssertKernelVersion=<1.0') +# syscall-filter +systemd-analyze syscall-filter >/dev/null +systemd-analyze syscall-filter @chown @sync +systemd-analyze syscall-filter @sync @sync @sync +(! systemd-analyze syscall-filter @chown @sync @foobar) +# filesystems (requires libbpf support) +if systemctl --version | grep "+BPF_FRAMEWORK"; then + systemd-analyze filesystems >/dev/null + systemd-analyze filesystems @basic-api + systemd-analyze filesystems @basic-api @basic-api @basic-api + (! systemd-analyze filesystems @basic-api @basic-api @foobar @basic-api) +fi +# calendar +systemd-analyze calendar '*-2-29 0:0:0' +systemd-analyze calendar --iterations=5 '*-2-29 0:0:0' +systemd-analyze calendar '*-* *:*:*' +systemd-analyze calendar --iterations=5 '*-* *:*:*' +systemd-analyze calendar --iterations=50 '*-* *:*:*' +systemd-analyze calendar --iterations=0 '*-* *:*:*' +systemd-analyze calendar --iterations=5 '01-01-22 01:00:00' +systemd-analyze calendar --base-time=yesterday --iterations=5 '*-* *:*:*' +(! systemd-analyze calendar --iterations=0 '*-* 99:*:*') +(! systemd-analyze calendar --base-time=never '*-* *:*:*') +(! systemd-analyze calendar 1) +(! systemd-analyze calendar "") +# timestamp +systemd-analyze timestamp now +systemd-analyze timestamp -- -1 +systemd-analyze timestamp yesterday now tomorrow +(! systemd-analyze timestamp yesterday never tomorrow) +(! systemd-analyze timestamp 1) +(! systemd-analyze timestamp '*-2-29 0:0:0') +(! systemd-analyze timestamp "") +# timespan +systemd-analyze timespan 1 +systemd-analyze timespan 1s 300s '1year 0.000001s' +(! systemd-analyze timespan 1s 300s aaaaaa '1year 0.000001s') +(! systemd-analyze timespan -- -1) +(! systemd-analyze timespan '*-2-29 0:0:0') +(! systemd-analyze timespan "") +# cat-config +systemd-analyze cat-config systemd/system.conf >/dev/null +systemd-analyze cat-config /etc/systemd/system.conf >/dev/null +systemd-analyze cat-config systemd/system.conf systemd/journald.conf >/dev/null +systemd-analyze cat-config systemd/system.conf foo/bar systemd/journald.conf >/dev/null +systemd-analyze cat-config foo/bar +systemd-analyze cat-config --tldr systemd/system.conf >/dev/null +systemd-analyze cat-config --tldr /etc/systemd/system.conf >/dev/null +systemd-analyze cat-config --tldr systemd/system.conf systemd/journald.conf >/dev/null +systemd-analyze cat-config --tldr systemd/system.conf foo/bar systemd/journald.conf >/dev/null +systemd-analyze cat-config --tldr foo/bar +# security +systemd-analyze security +systemd-analyze security --json=off +systemd-analyze security --json=pretty | jq +systemd-analyze security --json=short | jq + +if [[ ! -v ASAN_OPTIONS ]]; then + # check that systemd-analyze cat-config paths work in a chroot + mkdir -p /tmp/root + mount --bind / /tmp/root + systemd-analyze cat-config systemd/system-preset >/tmp/out1 + chroot /tmp/root systemd-analyze cat-config systemd/system-preset >/tmp/out2 + diff /tmp/out{1,2} +fi + +# verify +mkdir -p /tmp/img/usr/lib/systemd/system/ +mkdir -p /tmp/img/opt/ + +touch /tmp/img/opt/script0.sh +chmod +x /tmp/img/opt/script0.sh + +cat <<EOF >/tmp/img/usr/lib/systemd/system/testfile.service +[Service] +ExecStart = /opt/script0.sh +EOF + +set +e +# Default behaviour is to recurse through all dependencies when unit is loaded +(! systemd-analyze verify --root=/tmp/img/ testfile.service) + +# As above, recurses through all dependencies when unit is loaded +(! systemd-analyze verify --recursive-errors=yes --root=/tmp/img/ testfile.service) + +# Recurses through unit file and its direct dependencies when unit is loaded +(! systemd-analyze verify --recursive-errors=one --root=/tmp/img/ testfile.service) + +set -e + +# zero exit status since dependencies are ignored when unit is loaded +systemd-analyze verify --recursive-errors=no --root=/tmp/img/ testfile.service + +rm /tmp/img/usr/lib/systemd/system/testfile.service + +cat <<EOF >/tmp/testfile.service +[Unit] +foo = bar + +[Service] +ExecStart = echo hello +EOF + +cat <<EOF >/tmp/testfile2.service +[Unit] +Requires = testfile.service + +[Service] +ExecStart = echo hello +EOF + +# Zero exit status since no additional dependencies are recursively loaded when the unit file is loaded +systemd-analyze verify --recursive-errors=no /tmp/testfile2.service + +set +e +# Non-zero exit status since all associated dependencies are recursively loaded when the unit file is loaded +(! systemd-analyze verify --recursive-errors=yes /tmp/testfile2.service) +set -e + +rm /tmp/testfile.service +rm /tmp/testfile2.service + +cat <<EOF >/tmp/sample.service +[Unit] +Description = A Sample Service + +[Service] +ExecStart = echo hello +Slice=support.slice +EOF + +# Zero exit status since no additional dependencies are recursively loaded when the unit file is loaded +systemd-analyze verify --recursive-errors=no /tmp/sample.service + +cat <<EOF >/tmp/testfile.service +[Service] +ExecStart = echo hello +DeviceAllow=/dev/sda +EOF + +# Prevent regression from #13380 and #20859 where we can't verify hidden files +cp /tmp/testfile.service /tmp/.testfile.service + +systemd-analyze verify /tmp/.testfile.service + +rm /tmp/.testfile.service + +# Alias a unit file's name on disk (see #20061) +cp /tmp/testfile.service /tmp/testsrvc + +(! systemd-analyze verify /tmp/testsrvc) + +systemd-analyze verify /tmp/testsrvc:alias.service + +# Zero exit status since the value used for comparison determine exposure to security threats is by default 100 +systemd-analyze security --offline=true /tmp/testfile.service + +#The overall exposure level assigned to the unit is greater than the set threshold +(! systemd-analyze security --threshold=90 --offline=true /tmp/testfile.service) + +# Ensure we print the list of ACLs, see https://github.com/systemd/systemd/issues/23185 +systemd-analyze security --offline=true /tmp/testfile.service | grep -q -F "/dev/sda" + +rm /tmp/testfile.service + +cat <<EOF >/tmp/img/usr/lib/systemd/system/testfile.service +[Service] +ExecStart = echo hello +PrivateNetwork = yes +PrivateDevices = yes +PrivateUsers = yes +EOF + +# The new overall exposure level assigned to the unit is less than the set thresholds +# Verifies that the --offline= option works with --root= +systemd-analyze security --threshold=90 --offline=true --root=/tmp/img/ testfile.service + +cat <<EOF >/tmp/foo@.service +[Service] +ExecStart=ls +EOF + +cat <<EOF >/tmp/hoge@test.service +[Service] +ExecStart=ls +EOF + +# issue #30357 +pushd /tmp +systemd-analyze verify foo@bar.service +systemd-analyze verify foo@.service +systemd-analyze verify hoge@test.service +(! systemd-analyze verify hoge@nonexist.service) +(! systemd-analyze verify hoge@.service) +popd +pushd / +systemd-analyze verify tmp/foo@bar.service +systemd-analyze verify tmp/foo@.service +systemd-analyze verify tmp/hoge@test.service +(! systemd-analyze verify tmp/hoge@nonexist.service) +(! systemd-analyze verify tmp/hoge@.service) +popd +pushd /usr +systemd-analyze verify ../tmp/foo@bar.service +systemd-analyze verify ../tmp/foo@.service +systemd-analyze verify ../tmp/hoge@test.service +(! systemd-analyze verify ../tmp/hoge@nonexist.service) +(! systemd-analyze verify ../tmp/hoge@.service) +popd +systemd-analyze verify /tmp/foo@bar.service +systemd-analyze verify /tmp/foo@.service +systemd-analyze verify /tmp/hoge@test.service +(! systemd-analyze verify /tmp/hoge@nonexist.service) +(! systemd-analyze verify /tmp/hoge@.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 +# assigned to that id in the .json file is 6000. This increased weight means that when the "PrivateNetwork" key is +# set to 'yes' (as above in the case of testfile.service) in the content of the unit file, the overall exposure +# level for the unit file should decrease to account for that increased weight. +cat <<EOF >/tmp/testfile.json +{"UserOrDynamicUser": + {"description_bad": "Service runs as root user", + "weight": 0, + "range": 10 + }, +"SupplementaryGroups": + {"description_good": "Service has no supplementary groups", + "description_bad": "Service runs with supplementary groups", + "description_na": "Service runs as root, option does not matter", + "weight": 200, + "range": 1 + }, +"PrivateDevices": + {"description_good": "Service has no access to hardware devices", + "description_bad": "Service potentially has access to hardware devices", + "weight": 1000, + "range": 1 + }, +"PrivateMounts": + {"description_good": "Service cannot install system mounts", + "description_bad": "Service may install system mounts", + "weight": 1000, + "range": 1 + }, +"PrivateNetwork": + {"description_good": "Service doesn't have access to the host's network", + "description_bad": "Service has access to the host's network", + "weight": 6000, + "range": 1 + }, +"PrivateTmp": + {"description_good": "Service has no access to other software's temporary files", + "description_bad": "Service has access to other software's temporary files", + "weight": 1000, + "range": 1 + }, +"PrivateUsers": + {"description_good": "Service does not have access to other users", + "description_bad": "Service has access to other users", + "weight": 1000, + "range": 1 + }, +"ProtectControlGroups": + {"description_good": "Service cannot modify the control group file system", + "description_bad": "Service may modify the control group file system", + "weight": 1000, + "range": 1 + }, +"ProtectKernelModules": + {"description_good": "Service cannot load or read kernel modules", + "description_bad": "Service may load or read kernel modules", + "weight": 1000, + "range": 1 + }, +"ProtectKernelTunables": + {"description_good": "Service cannot alter kernel tunables (/proc/sys, …)", + "description_bad": "Service may alter kernel tunables", + "weight": 1000, + "range": 1 + }, +"ProtectKernelLogs": + {"description_good": "Service cannot read from or write to the kernel log ring buffer", + "description_bad": "Service may read from or write to the kernel log ring buffer", + "weight": 1000, + "range": 1 + }, +"ProtectClock": + {"description_good": "Service cannot write to the hardware clock or system clock", + "description_bad": "Service may write to the hardware clock or system clock", + "weight": 1000, + "range": 1 + }, +"ProtectHome": + {"weight": 1000, + "range": 10 + }, +"ProtectHostname": + {"description_good": "Service cannot change system host/domainname", + "description_bad": "Service may change system host/domainname", + "weight": 50, + "range": 1 + }, +"ProtectSystem": + {"weight": 1000, + "range": 10 + }, +"RootDirectoryOrRootImage": + {"description_good": "Service has its own root directory/image", + "description_bad": "Service runs within the host's root directory", + "weight": 200, + "range": 1 + }, +"LockPersonality": + {"description_good": "Service cannot change ABI personality", + "description_bad": "Service may change ABI personality", + "weight": 100, + "range": 1 + }, +"MemoryDenyWriteExecute": + {"description_good": "Service cannot create writable executable memory mappings", + "description_bad": "Service may create writable executable memory mappings", + "weight": 100, + "range": 1 + }, +"NoNewPrivileges": + {"description_good": "Service processes cannot acquire new privileges", + "description_bad": "Service processes may acquire new privileges", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_ADMIN": + {"description_good": "Service has no administrator privileges", + "description_bad": "Service has administrator privileges", + "weight": 1500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SET_UID_GID_PCAP": + {"description_good": "Service cannot change UID/GID identities/capabilities", + "description_bad": "Service may change UID/GID identities/capabilities", + "weight": 1500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_PTRACE": + {"description_good": "Service has no ptrace() debugging abilities", + "description_bad": "Service has ptrace() debugging abilities", + "weight": 1500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_TIME": + {"description_good": "Service processes cannot change the system clock", + "description_bad": "Service processes may change the system clock", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_NET_ADMIN": + {"description_good": "Service has no network configuration privileges", + "description_bad": "Service has network configuration privileges", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_RAWIO": + {"description_good": "Service has no raw I/O access", + "description_bad": "Service has raw I/O access", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_MODULE": + {"description_good": "Service cannot load kernel modules", + "description_bad": "Service may load kernel modules", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_AUDIT": + {"description_good": "Service has no audit subsystem access", + "description_bad": "Service has audit subsystem access", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYSLOG": + {"description_good": "Service has no access to kernel logging", + "description_bad": "Service has access to kernel logging", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_NICE_RESOURCE": + {"description_good": "Service has no privileges to change resource use parameters", + "description_bad": "Service has privileges to change resource use parameters", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_MKNOD": + {"description_good": "Service cannot create device nodes", + "description_bad": "Service may create device nodes", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_CHOWN_FSETID_SETFCAP": + {"description_good": "Service cannot change file ownership/access mode/capabilities", + "description_bad": "Service may change file ownership/access mode/capabilities unrestricted", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_DAC_FOWNER_IPC_OWNER": + {"description_good": "Service cannot override UNIX file/IPC permission checks", + "description_bad": "Service may override UNIX file/IPC permission checks", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_KILL": + {"description_good": "Service cannot send UNIX signals to arbitrary processes", + "description_bad": "Service may send UNIX signals to arbitrary processes", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_NET_BIND_SERVICE_BROADCAST_RAW": + {"description_good": "Service has no elevated networking privileges", + "description_bad": "Service has elevated networking privileges", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_BOOT": + {"description_good": "Service cannot issue reboot()", + "description_bad": "Service may issue reboot()", + "weight": 100, + "range": 1 + }, +"CapabilityBoundingSet_CAP_MAC": + {"description_good": "Service cannot adjust SMACK MAC", + "description_bad": "Service may adjust SMACK MAC", + "weight": 100, + "range": 1 + }, +"CapabilityBoundingSet_CAP_LINUX_IMMUTABLE": + {"description_good": "Service cannot mark files immutable", + "description_bad": "Service may mark files immutable", + "weight": 75, + "range": 1 + }, +"CapabilityBoundingSet_CAP_IPC_LOCK": + {"description_good": "Service cannot lock memory into RAM", + "description_bad": "Service may lock memory into RAM", + "weight": 50, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_CHROOT": + {"description_good": "Service cannot issue chroot()", + "description_bad": "Service may issue chroot()", + "weight": 50, + "range": 1 + }, +"CapabilityBoundingSet_CAP_BLOCK_SUSPEND": + {"description_good": "Service cannot establish wake locks", + "description_bad": "Service may establish wake locks", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_WAKE_ALARM": + {"description_good": "Service cannot program timers that wake up the system", + "description_bad": "Service may program timers that wake up the system", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_LEASE": + {"description_good": "Service cannot create file leases", + "description_bad": "Service may create file leases", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_TTY_CONFIG": + {"description_good": "Service cannot issue vhangup()", + "description_bad": "Service may issue vhangup()", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_PACCT": + {"description_good": "Service cannot use acct()", + "description_bad": "Service may use acct()", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_BPF": + {"description_good": "Service may load BPF programs", + "description_bad": "Service may not load BPF programs", + "weight": 25, + "range": 1 + }, +"UMask": + {"weight": 100, + "range": 10 + }, +"KeyringMode": + {"description_good": "Service doesn't share key material with other services", + "description_bad": "Service shares key material with other service", + "weight": 1000, + "range": 1 + }, +"ProtectProc": + {"description_good": "Service has restricted access to process tree(/proc hidepid=)", + "description_bad": "Service has full access to process tree(/proc hidepid=)", + "weight": 1000, + "range": 3 + }, +"ProcSubset": + {"description_good": "Service has no access to non-process/proc files(/proc subset=)", + "description_bad": "Service has full access to non-process/proc files(/proc subset=)", + "weight": 10, + "range": 1 + }, +"NotifyAccess": + {"description_good": "Service child processes cannot alter service state", + "description_bad": "Service child processes may alter service state", + "weight": 1000, + "range": 1 + }, +"RemoveIPC": + {"description_good": "Service user cannot leave SysV IPC objects around", + "description_bad": "Service user may leave SysV IPC objects around", + "description_na": "Service runs as root, option does not apply", + "weight": 100, + "range": 1 + }, +"Delegate": + {"description_good": "Service does not maintain its own delegated control group subtree", + "description_bad": "Service maintains its own delegated control group subtree", + "weight": 100, + "range": 1 + }, +"RestrictRealtime": + {"description_good": "Service realtime scheduling access is restricted", + "description_bad": "Service may acquire realtime scheduling", + "weight": 500, + "range": 1 + }, +"RestrictSUIDSGID": + {"description_good": "SUID/SGIDfilecreationbyserviceisrestricted", + "description_bad": "ServicemaycreateSUID/SGIDfiles", + "weight": 1000, + "range": 1 + }, +"RestrictNamespaces_user": + {"description_good": "Servicecannotcreateusernamespaces", + "description_bad": "Servicemaycreateusernamespaces", + "weight": 1500, + "range": 1 + }, +"RestrictNamespaces_mnt": + {"description_good": "Service cannot create file system namespaces", + "description_bad": "Service may create file system namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_ipc": + {"description_good": "Service cannot create IPC namespaces", + "description_bad": "Service may create IPC namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_pid": + {"description_good": "Service cannot create process namespaces", + "description_bad": "Service may create process namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_cgroup": + {"description_good": "Service cannot create cgroup namespaces", + "description_bad": "Service may create cgroup namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_net": + {"description_good": "Service cannot create network namespaces", + "description_bad": "Service may create network namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_uts": + {"description_good": "Service cannot create hostname namespaces", + "description_bad": "Service may create hostname namespaces", + "weight": 100, + "range": 1 + }, +"RestrictAddressFamilies_AF_INET_INET6": + {"description_good": "Service cannot allocate Internet sockets", + "description_bad": "Service may allocate Internet sockets", + "weight": 1500, + "range": 1 + }, +"RestrictAddressFamilies_AF_UNIX": + {"description_good": "Service cannot allocate local sockets", + "description_bad": "Service may allocate local sockets", + "weight": 25, + "range": 1 + }, +"RestrictAddressFamilies_AF_NETLINK": + {"description_good": "Service cannot allocate netlink sockets", + "description_bad": "Service may allocate netlink sockets", + "weight": 200, + "range": 1 + }, +"RestrictAddressFamilies_AF_PACKET": + {"description_good": "Service cannot allocate packet sockets", + "description_bad": "Service may allocate packet sockets", + "weight": 1000, + "range": 1 + }, +"RestrictAddressFamilies_OTHER": + {"description_good": "Service cannot allocate exotic sockets", + "description_bad": "Service may allocate exotic sockets", + "weight": 1250, + "range": 1 + }, +"SystemCallArchitectures": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_swap": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_obsolete": + {"weight": 250, + "range": 10 + }, +"SystemCallFilter_clock": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_cpu_emulation": + {"weight": 250, + "range": 10 + }, +"SystemCallFilter_debug": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_mount": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_module": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_raw_io": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_reboot": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_privileged": + {"weight": 700, + "range": 10 + }, +"SystemCallFilter_resources": + {"weight": 700, + "range": 10 + }, +"IPAddressDeny": + {"weight": 1000, + "range": 10 + }, +"DeviceAllow": + {"weight": 1000, + "range": 10 + }, +"AmbientCapabilities": + {"description_good": "Service process does not receive ambient capabilities", + "description_bad": "Service process receives ambient capabilities", + "weight": 500, + "range": 1 + }, +"INVALID_ID": + {"weight": 1000, + "range": 10 + } +} +EOF + +# Reads in custom security requirements from the parsed .json file and uses these for comparison +systemd-analyze security --threshold=90 --offline=true \ + --security-policy=/tmp/testfile.json \ + --root=/tmp/img/ testfile.service + +# The strict profile adds a lot of sanboxing options +systemd-analyze security --threshold=25 --offline=true \ + --security-policy=/tmp/testfile.json \ + --profile=strict \ + --root=/tmp/img/ testfile.service + +# The trusted profile doesn't add any sanboxing options +(! systemd-analyze security --threshold=25 --offline=true \ + --security-policy=/tmp/testfile.json \ + --profile=/usr/lib/systemd/portable/profile/trusted/service.conf \ + --root=/tmp/img/ testfile.service) + +(! systemd-analyze security --threshold=50 --offline=true \ + --security-policy=/tmp/testfile.json \ + --root=/tmp/img/ testfile.service) + +rm /tmp/img/usr/lib/systemd/system/testfile.service + +if systemd-analyze --version | grep -q -F "+ELFUTILS"; then + systemd-analyze inspect-elf --json=short /lib/systemd/systemd | grep -q -F '"elfType":"executable"' +fi + +systemd-analyze --threshold=90 security systemd-journald.service + +# issue 23663 +check() {( + set +x + output=$(systemd-analyze security --offline="${2?}" "${3?}" | grep -F 'SystemCallFilter=') + assert_in "System call ${1?} list" "$output" + assert_in "[+✓] SystemCallFilter=~@swap" "$output" + assert_in "[+✓] SystemCallFilter=~@resources" "$output" + assert_in "[+✓] SystemCallFilter=~@reboot" "$output" + assert_in "[+✓] SystemCallFilter=~@raw-io" "$output" + assert_in "[-✗] SystemCallFilter=~@privileged" "$output" + assert_in "[+✓] SystemCallFilter=~@obsolete" "$output" + assert_in "[+✓] SystemCallFilter=~@mount" "$output" + assert_in "[+✓] SystemCallFilter=~@module" "$output" + assert_in "[+✓] SystemCallFilter=~@debug" "$output" + assert_in "[+✓] SystemCallFilter=~@cpu-emulation" "$output" + assert_in "[-✗] SystemCallFilter=~@clock" "$output" +)} + +export -n SYSTEMD_LOG_LEVEL + +mkdir -p /run/systemd/system +cat >/run/systemd/system/allow-list.service <<EOF +[Service] +ExecStart=false +SystemCallFilter=@system-service +SystemCallFilter=~@resources:ENOANO @privileged +SystemCallFilter=@clock +EOF + +cat >/run/systemd/system/deny-list.service <<EOF +[Service] +ExecStart=false +SystemCallFilter=~@known +SystemCallFilter=@system-service +SystemCallFilter=~@resources:ENOANO @privileged +SystemCallFilter=@clock +EOF + +systemctl daemon-reload + +check allow yes /run/systemd/system/allow-list.service +check allow no allow-list.service +check deny yes /run/systemd/system/deny-list.service +check deny no deny-list.service + +output=$(systemd-run -p "SystemCallFilter=@system-service" -p "SystemCallFilter=~@resources:ENOANO @privileged" -p "SystemCallFilter=@clock" sleep 60 2>&1) +name=$(echo "$output" | awk '{ print $4 }' | cut -d';' -f1) + +check allow yes /run/systemd/transient/"$name" +check allow no "$name" + +output=$(systemd-run -p "SystemCallFilter=~@known" -p "SystemCallFilter=@system-service" -p "SystemCallFilter=~@resources:ENOANO @privileged" -p "SystemCallFilter=@clock" sleep 60 2>&1) +name=$(echo "$output" | awk '{ print $4 }' | cut -d';' -f1) + +check deny yes /run/systemd/transient/"$name" +check deny no "$name" + +# Let's also test the "image-policy" verb + +systemd-analyze image-policy '*' 2>&1 | grep -q -F "Long form: =verity+signed+encrypted+unprotected+unused+absent" +systemd-analyze image-policy '-' 2>&1 | grep -q -F "Long form: =unused+absent" +systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -F "Long form: usr=verity:home=encrypted:=unused+absent" +systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -e '^home \+encrypted \+' +systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -e '^usr \+verity \+' +systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -e '^root \+ignore \+' +systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -e '^usr-verity \+unprotected \+' + +(! systemd-analyze image-policy 'doedel') + +# Output is very hard to predict, but let's run it for coverage anyway +systemd-analyze pcrs +systemd-analyze pcrs --json=pretty +systemd-analyze pcrs 14 7 0 ima + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-66-deviceisolation.service b/test/units/testsuite-66-deviceisolation.service new file mode 100644 index 0000000..2d815a9 --- /dev/null +++ b/test/units/testsuite-66-deviceisolation.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Service that uses device isolation + +[Service] +DevicePolicy=strict +DeviceAllow=/dev/null r +StandardOutput=file:/tmp/testsuite66serviceresults +ExecStartPre=rm -f /tmp/testsuite66serviceresults +ExecStart=/bin/bash -c "while true; do sleep 0.01 && echo meow >/dev/null && echo thisshouldnotbehere; done" diff --git a/test/units/testsuite-66.service b/test/units/testsuite-66.service new file mode 100644 index 0000000..7e9dc3b --- /dev/null +++ b/test/units/testsuite-66.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TESTSUITE-66-DEVICEISOLATION + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-66.sh b/test/units/testsuite-66.sh new file mode 100755 index 0000000..147335a --- /dev/null +++ b/test/units/testsuite-66.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +RESULTS_FILE=/tmp/testsuite66serviceresults + +systemd-analyze log-level debug + +systemctl start testsuite-66-deviceisolation.service + +sleep 5 +grep -q "Operation not permitted" "$RESULTS_FILE" + +systemctl daemon-reload +systemctl daemon-reexec + +systemctl stop testsuite-66-deviceisolation.service + +grep -q "thisshouldnotbehere" "$RESULTS_FILE" && exit 42 + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-67.service b/test/units/testsuite-67.service new file mode 100644 index 0000000..82f998e --- /dev/null +++ b/test/units/testsuite-67.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-67-INTEGRITY +After=multi-user.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-67.sh b/test/units/testsuite-67.sh new file mode 100755 index 0000000..a42fd66 --- /dev/null +++ b/test/units/testsuite-67.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -euxo pipefail + +export DM_NAME="integrity_test" +export FULL_DM_DEV_NAME="/dev/mapper/${DM_NAME}" +export FS_UUID="01234567-ffff-eeee-eeee-0123456789ab" +export GEN="/var/run/systemd/generator" + +image_dir="" + +cleanup() +{ + if [ -z "${image_dir}" ]; then + return + fi + + if [ -f "${image_dir}/image" ]; then + if [ -e "${FULL_DM_DEV_NAME}" ]; then + integritysetup close "${DM_NAME}" + fi + losetup -d "${loop}" + fi + + rm -rf "${image_dir}" +} + +trap cleanup EXIT + +build_integrity_tab() +{ +cat <<EOF >"/etc/integritytab" +${DM_NAME} ${loop} - integrity-algorithm=$1 +EOF +} + +image_dir="$(mktemp -d -t -p / integrity.tmp.XXXXXX)" +if [ -z "${image_dir}" ] || [ ! -d "${image_dir}" ]; then + echo "mktemp under / failed" + exit 1 +fi + +dd if=/dev/zero of="${image_dir}/image" bs=1048576 count=64 || exit 1 +dd if=/dev/zero of="${image_dir}/data" bs=1048576 count=64 || exit 1 +loop="$(losetup --show -f "${image_dir}/image")" + +if [[ ! -e ${loop} ]]; then + echo "Loopback device created not found!" + exit 1 +fi + +# Do one iteration with a separate data device, to test those branches +separate_data=1 + +for algorithm in crc32c crc32 sha1 sha256 +do + if [ "${separate_data}" -eq 1 ]; then + data_option="--data-device=${image_dir}/data" + else + data_option="" + fi + integritysetup format "${loop}" --batch-mode -I "${algorithm}" "${data_option}" || exit 1 + integritysetup open -I "${algorithm}" "${loop}" "${DM_NAME}" "${data_option}" || exit 1 + mkfs.ext4 -U "${FS_UUID}" "${FULL_DM_DEV_NAME}" || exit 1 + + # Give userspace time to handle udev events for new FS showing up ... + udevadm settle + + integritysetup close "${DM_NAME}" || exit 1 + + # create integritytab, generate units, start service + if [ "${separate_data}" -eq 1 ]; then + data_option=",data-device=${image_dir}/data" + else + data_option="" + fi + build_integrity_tab "${algorithm}${data_option}" + + # Cause the generator to re-run + systemctl daemon-reload || exit 1 + + # Check for existence of unit files... + if [[ ! -e "/run/systemd/generator/systemd-integritysetup@${DM_NAME}.service" ]]; then + echo "Service file does not exist!" + exit 1 + fi + + # Make sure we are in a consistent state, e.g. not already active before we start + systemctl stop systemd-integritysetup@"${DM_NAME}".service || exit 1 + systemctl start systemd-integritysetup@"${DM_NAME}".service || exit 1 + # Reset the start-limit counters, as we're going to restart the service a couple of times + systemctl reset-failed systemd-integritysetup@"${DM_NAME}".service + + # Check the signature on the FS to ensure we can retrieve it and that is matches + if [ -e "${FULL_DM_DEV_NAME}" ]; then + # If a separate device is used for the metadata storage, then blkid will return one of the loop devices + if [ "${separate_data}" -eq 1 ]; then + dev_name="$(integritysetup status ${DM_NAME} | grep '^\s*device:' | awk '{print $2}')" + else + dev_name="${FULL_DM_DEV_NAME}" + fi + if [ "${dev_name}" != "$(blkid -U "${FS_UUID}")" ]; then + echo "Failed to locate FS with matching UUID!" + exit 1 + fi + else + echo "Failed to bring up integrity device!" + exit 1 + fi + + systemctl stop systemd-integritysetup@"${DM_NAME}".service || exit 1 + + if [ -e "${FULL_DM_DEV_NAME}" ]; then + echo "Expecting ${FULL_DM_DEV_NAME} to not exist after stopping unit!" + exit 1 + fi + + separate_data=0 +done + +touch /testok diff --git a/test/units/testsuite-68.service b/test/units/testsuite-68.service new file mode 100644 index 0000000..2d86e1f --- /dev/null +++ b/test/units/testsuite-68.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-68-PROPAGATE-EXIT-STATUS + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh diff --git a/test/units/testsuite-68.sh b/test/units/testsuite-68.sh new file mode 100755 index 0000000..11da48a --- /dev/null +++ b/test/units/testsuite-68.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# Wait for a service to enter a state within a timeout period, if it doesn't +# enter the desired state within the timeout period then this function will +# exit the test case with a non zero exit code. +wait_on_state_or_fail() { + service=$1 + expected_state=$2 + timeout=$3 + + state=$(systemctl show "$service" --property=ActiveState --value) + while [ "$state" != "$expected_state" ]; do + if [ "$timeout" = "0" ]; then + systemd-analyze log-level info + exit 1 + fi + timeout=$((timeout - 1)) + sleep 1 + state=$(systemctl show "$service" --property=ActiveState --value) + done +} + +systemd-analyze log-level debug + +cat >/run/systemd/system/testservice-failure-68.service <<EOF +[Unit] +OnFailure=testservice-failure-exit-handler-68.service + +[Service] +ExecStart=sh -c "exit 1" +EOF + +cat >/run/systemd/system/testservice-failure-68-template.service <<EOF +[Unit] +OnFailure=testservice-failure-exit-handler-68-template@%n.service + +[Service] +ExecStart=sh -c "exit 1" +EOF + +cat >/run/systemd/system/testservice-success-68.service <<EOF +[Unit] +OnSuccess=testservice-success-exit-handler-68.service + +[Service] +ExecStart=sh -c "exit 0" +EOF + +cat >/run/systemd/system/testservice-success-68-template.service <<EOF +[Unit] +OnSuccess=testservice-success-exit-handler-68-template@%n.service + +[Service] +ExecStart=sh -c "exit 0" +EOF + +# Script to check that when an OnSuccess= dependency fires, the correct +# MONITOR* env variables are passed. +cat >/tmp/check_on_success.sh <<"EOF" +#!/bin/sh + +set -ex +env | sort +if [ "$MONITOR_SERVICE_RESULT" != "success" ]; then + echo "MONITOR_SERVICE_RESULT was '$MONITOR_SERVICE_RESULT', expected 'success'" + exit 1 +fi + +if [ "$MONITOR_EXIT_CODE" != "exited" ]; then + echo "MONITOR_EXIT_CODE was '$MONITOR_EXIT_CODE', expected 'exited'" + exit 1 +fi + +if [ "$MONITOR_EXIT_STATUS" != "0" ]; then + echo "MONITOR_EXIT_STATUS was '$MONITOR_EXIT_STATUS', expected '0'" + exit 1 +fi + +if [ -z "$MONITOR_INVOCATION_ID" ]; then + echo "MONITOR_INVOCATION_ID unset" + exit 1 +fi + +if [ "$MONITOR_UNIT" != "testservice-success-68.service" ] && + [ "$MONITOR_UNIT" != "testservice-success-68-template.service" ] && + [ "$MONITOR_UNIT" != "testservice-transient-success-68.service" ]; then + + echo "MONITOR_UNIT was '$MONITOR_UNIT', expected 'testservice[-transient]-success-68[-template].service'" + exit 1 +fi + +exit 0 +EOF +chmod +x /tmp/check_on_success.sh + +cat >/run/systemd/system/testservice-success-exit-handler-68.service <<EOF +[Service] +ExecStartPre=/tmp/check_on_success.sh +ExecStart=/tmp/check_on_success.sh +EOF + +cp /run/systemd/system/testservice-success-exit-handler-68.service \ + /run/systemd/system/testservice-transient-success-exit-handler-68.service + +# Template version. +cat >/run/systemd/system/testservice-success-exit-handler-68-template@.service <<EOF +[Service] +ExecStartPre=echo "triggered by %i" +ExecStartPre=/tmp/check_on_success.sh +ExecStart=/tmp/check_on_success.sh +EOF + +# Script to check that when an OnFailure= dependency fires, the correct +# MONITOR* env variables are passed. +cat >/tmp/check_on_failure.sh <<"EOF" +#!/bin/sh + +set -ex +env | sort +if [ "$MONITOR_SERVICE_RESULT" != "exit-code" ]; then + echo "MONITOR_SERVICE_RESULT was '$MONITOR_SERVICE_RESULT', expected 'exit-code'" + exit 1 +fi + +if [ "$MONITOR_EXIT_CODE" != "exited" ]; then + echo "MONITOR_EXIT_CODE was '$MONITOR_EXIT_CODE', expected 'exited'" + exit 1 +fi + +if [ "$MONITOR_EXIT_STATUS" != "1" ]; then + echo "MONITOR_EXIT_STATUS was '$MONITOR_EXIT_STATUS', expected '1'" + exit 1 +fi + +if [ -z "$MONITOR_INVOCATION_ID" ]; then + echo "MONITOR_INVOCATION_ID unset" + exit 1 +fi + +if [ "$MONITOR_UNIT" != "testservice-failure-68.service" ] && + [ "$MONITOR_UNIT" != "testservice-failure-68-template.service" ] && + [ "$MONITOR_UNIT" != "testservice-transient-failure-68.service" ]; then + + echo "MONITOR_UNIT was '$MONITOR_UNIT', expected 'testservice[-transient]-failure-68[-template].service'" + exit 1 +fi + +exit 0 +EOF +chmod +x /tmp/check_on_failure.sh + + +cat >/run/systemd/system/testservice-failure-exit-handler-68.service <<EOF +[Service] +# repeat the check to make sure that values are set correctly on repeated invocations +Type=oneshot +ExecStartPre=/tmp/check_on_failure.sh +ExecStartPre=/tmp/check_on_failure.sh +ExecStart=/tmp/check_on_failure.sh +ExecStart=/tmp/check_on_failure.sh +ExecStartPost=test -z '$MONITOR_SERVICE_RESULT' +EOF + +cp /run/systemd/system/testservice-failure-exit-handler-68.service \ + /run/systemd/system/testservice-transient-failure-exit-handler-68.service + +# Template version. +cat >/run/systemd/system/testservice-failure-exit-handler-68-template@.service <<EOF +[Service] +Type=oneshot +ExecStartPre=echo "triggered by %i" +ExecStartPre=/tmp/check_on_failure.sh +ExecStartPre=/tmp/check_on_failure.sh +ExecStart=/tmp/check_on_failure.sh +ExecStart=/tmp/check_on_failure.sh +ExecStartPost=test -z '$MONITOR_SERVICE_RESULT' +EOF + +systemctl daemon-reload + +: "-------I----------------------------------------------------" +systemctl start testservice-failure-68.service +wait_on_state_or_fail "testservice-failure-exit-handler-68.service" "inactive" "10" + +: "-------II---------------------------------------------------" +systemctl start testservice-success-68.service +wait_on_state_or_fail "testservice-success-exit-handler-68.service" "inactive" "10" + +# Test some transient units since these exit very quickly. +: "-------III--------------------------------------------------" +systemd-run --unit=testservice-transient-success-68 \ + --property=OnSuccess=testservice-transient-success-exit-handler-68.service \ + sh -c "exit 0" +wait_on_state_or_fail "testservice-success-exit-handler-68.service" "inactive" "10" + +: "-------IIII-------------------------------------------------" +systemd-run --unit=testservice-transient-failure-68 \ + --property=OnFailure=testservice-transient-failure-exit-handler-68.service \ + sh -c "exit 1" +wait_on_state_or_fail "testservice-failure-exit-handler-68.service" "inactive" "10" + +# Test template handlers too +: "-------V---------------------------------------------------" +systemctl start testservice-success-68-template.service +wait_on_state_or_fail "testservice-success-exit-handler-68-template@testservice-success-68-template.service.service" "inactive" "10" + +: "-------VI----------------------------------------------------" +systemctl start testservice-failure-68-template.service +wait_on_state_or_fail "testservice-failure-exit-handler-68-template@testservice-failure-68-template.service.service" "inactive" "10" + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-69.service b/test/units/testsuite-69.service new file mode 100644 index 0000000..7aa0664 --- /dev/null +++ b/test/units/testsuite-69.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-69-SHUTDOWN + +[Service] +Type=oneshot +ExecStart=/bin/true diff --git a/test/units/testsuite-70.creds.sh b/test/units/testsuite-70.creds.sh new file mode 100755 index 0000000..e66bfd1 --- /dev/null +++ b/test/units/testsuite-70.creds.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +export SYSTEMD_LOG_LEVEL=debug + +# Ensure that sandboxing doesn't stop creds from being accessible +echo "test" > /tmp/testdata +systemd-creds encrypt /tmp/testdata /tmp/testdata.encrypted --with-key=tpm2 +# LoadCredentialEncrypted +systemd-run -p PrivateDevices=yes -p LoadCredentialEncrypted=testdata.encrypted:/tmp/testdata.encrypted --pipe --wait systemd-creds cat testdata.encrypted | cmp - /tmp/testdata +# SetCredentialEncrypted +systemd-run -p PrivateDevices=yes -p SetCredentialEncrypted=testdata.encrypted:"$(cat /tmp/testdata.encrypted)" --pipe --wait systemd-creds cat testdata.encrypted | cmp - /tmp/testdata + +rm -f /tmp/testdata diff --git a/test/units/testsuite-70.cryptenroll.sh b/test/units/testsuite-70.cryptenroll.sh new file mode 100755 index 0000000..3f8c14e --- /dev/null +++ b/test/units/testsuite-70.cryptenroll.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +cryptenroll_wipe_and_check() {( + set +o pipefail + + : >/tmp/cryptenroll.out + systemd-cryptenroll "$@" |& tee /tmp/cryptenroll.out + grep -qE "Wiped slot [[:digit:]]+" /tmp/cryptenroll.out +)} + +# There is an external issue with libcryptsetup on ppc64 that hits 95% of Ubuntu ppc64 test runs, so skip it +if [[ "$(uname -m)" == "ppc64le" ]]; then + echo "Skipping systemd-cryptenroll tests on ppc64le, see https://github.com/systemd/systemd/issues/27716" + exit 0 +fi + +export SYSTEMD_LOG_LEVEL=debug +IMAGE="$(mktemp /tmp/systemd-cryptenroll-XXX.image)" + +truncate -s 20M "$IMAGE" +echo -n password >/tmp/password +# Change file mode to avoid "/tmp/password has 0644 mode that is too permissive" messages +chmod 0600 /tmp/password +cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$IMAGE" /tmp/password + +# Enroll additional tokens, keys, and passwords to exercise the list and wipe stuff +systemd-cryptenroll --unlock-key-file=/tmp/password --tpm2-device=auto "$IMAGE" +NEWPASSWORD="" systemd-cryptenroll --unlock-key-file=/tmp/password --password "$IMAGE" +NEWPASSWORD=foo systemd-cryptenroll --unlock-key-file=/tmp/password --password "$IMAGE" +for _ in {0..9}; do + systemd-cryptenroll --unlock-key-file=/tmp/password --recovery-key "$IMAGE" +done +PASSWORD="" NEWPIN=123456 systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=true "$IMAGE" +# Do some basic checks before we start wiping stuff +systemd-cryptenroll "$IMAGE" +systemd-cryptenroll "$IMAGE" | grep password +systemd-cryptenroll "$IMAGE" | grep recovery +# Let's start wiping +cryptenroll_wipe_and_check "$IMAGE" --wipe=empty +(! cryptenroll_wipe_and_check "$IMAGE" --wipe=empty) +cryptenroll_wipe_and_check "$IMAGE" --wipe=empty,0 +PASSWORD=foo NEWPASSWORD=foo cryptenroll_wipe_and_check "$IMAGE" --wipe=0,0,empty,0,pkcs11,fido2,000,recovery,password --password +systemd-cryptenroll "$IMAGE" | grep password +(! systemd-cryptenroll "$IMAGE" | grep recovery) +# We shouldn't be able to wipe all keyslots without enrolling a new key first +(! systemd-cryptenroll "$IMAGE" --wipe=all) +PASSWORD=foo NEWPASSWORD=foo cryptenroll_wipe_and_check "$IMAGE" --password --wipe=all +# Check if the newly (and only) enrolled password works +(! systemd-cryptenroll --unlock-key-file=/tmp/password --recovery-key "$IMAGE") +(! PASSWORD="" systemd-cryptenroll --recovery-key "$IMAGE") +PASSWORD=foo systemd-cryptenroll --recovery-key "$IMAGE" + +systemd-cryptenroll --fido2-with-client-pin=false "$IMAGE" +systemd-cryptenroll --fido2-with-user-presence=false "$IMAGE" +systemd-cryptenroll --fido2-with-user-verification=false "$IMAGE" +systemd-cryptenroll --tpm2-pcrs=8 "$IMAGE" +systemd-cryptenroll --tpm2-pcrs=boot-loader-code+boot-loader-config "$IMAGE" + +(! systemd-cryptenroll --fido2-with-client-pin=false) +(! systemd-cryptenroll --fido2-with-user-presence=f "$IMAGE" /tmp/foo) +(! systemd-cryptenroll --fido2-with-client-pin=1234 "$IMAGE") +(! systemd-cryptenroll --fido2-with-user-presence=1234 "$IMAGE") +(! systemd-cryptenroll --fido2-with-user-verification=1234 "$IMAGE") +(! systemd-cryptenroll --tpm2-with-pin=1234 "$IMAGE") +(! systemd-cryptenroll --recovery-key --password "$IMAGE") +(! systemd-cryptenroll --password --recovery-key "$IMAGE") +(! systemd-cryptenroll --password --fido2-device=auto "$IMAGE") +(! systemd-cryptenroll --password --pkcs11-token-uri=auto "$IMAGE") +(! systemd-cryptenroll --password --tpm2-device=auto "$IMAGE") +(! systemd-cryptenroll --unlock-fido2-device=auto --unlock-fido2-device=auto "$IMAGE") +(! systemd-cryptenroll --unlock-fido2-device=auto --unlock-key-file=/tmp/unlock "$IMAGE") +(! systemd-cryptenroll --fido2-credential-algorithm=es512 "$IMAGE") +(! systemd-cryptenroll --tpm2-public-key-pcrs=key "$IMAGE") +(! systemd-cryptenroll --tpm2-pcrs=key "$IMAGE") +(! systemd-cryptenroll --tpm2-pcrs=44+8 "$IMAGE") +(! systemd-cryptenroll --tpm2-pcrs=hello "$IMAGE") +(! systemd-cryptenroll --wipe-slot "$IMAGE") +(! systemd-cryptenroll --wipe-slot=10240000 "$IMAGE") +(! systemd-cryptenroll --fido2-device=auto --unlock-fido2-device=auto "$IMAGE") + +rm -f "$IMAGE" diff --git a/test/units/testsuite-70.cryptsetup.sh b/test/units/testsuite-70.cryptsetup.sh new file mode 100755 index 0000000..4cd627f --- /dev/null +++ b/test/units/testsuite-70.cryptsetup.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +export SYSTEMD_LOG_LEVEL=debug + +cryptsetup_has_token_plugin_support() { + local plugin_path + + plugin_path="$(cryptsetup --help | sed -nr 's/.*LUKS2 external token plugin path: (.*)\./\1/p')/libcryptsetup-token-systemd-tpm2.so)" + cryptsetup --help | grep -q 'LUKS2 external token plugin support is compiled-in' && [[ -f "$plugin_path" ]] +} + +tpm_check_failure_with_wrong_pin() { + local testIMAGE="${1:?}" + local badpin="${2:?}" + local goodpin="${3:?}" + + # We need to be careful not to trigger DA lockout; allow 2 failures + tpm2_dictionarylockout -s -n 2 + (! PIN=$badpin systemd-cryptsetup attach test-volume "$testIMAGE" - tpm2-device=auto,headless=1) + # Verify the correct PIN works, to be sure the failure wasn't a DA lockout + PIN=$goodpin systemd-cryptsetup attach test-volume "$testIMAGE" - tpm2-device=auto,headless=1 + systemd-cryptsetup detach test-volume + # Clear/reset the DA lockout counter + tpm2_dictionarylockout -c +} + +at_exit() { + # Evict the TPM primary key that we persisted + if [[ -n "${PERSISTENT_HANDLE:-}" ]]; then + tpm2_evictcontrol -c "$PERSISTENT_HANDLE" + fi +} + +trap at_exit EXIT + +# Prepare a fresh disk image +IMAGE="$(mktemp /tmp/systemd-cryptsetup-XXX.IMAGE)" + +truncate -s 20M "$IMAGE" +echo -n passphrase >/tmp/passphrase +# Change file mode to avoid "/tmp/passphrase has 0644 mode that is too permissive" messages +chmod 0600 /tmp/passphrase +cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$IMAGE" /tmp/passphrase + +# Unlocking via keyfile +systemd-cryptenroll --unlock-key-file=/tmp/passphrase --tpm2-device=auto "$IMAGE" + +# Enroll unlock with default PCR policy +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +# Check with wrong PCR +tpm2_pcrextend 7:sha256=0000000000000000000000000000000000000000000000000000000000000000 +(! systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1) + +# Enroll unlock with PCR+PIN policy +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase NEWPIN=123456 systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=true "$IMAGE" +PIN=123456 systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +# Check failure with wrong PIN; try a few times to make sure we avoid DA lockout +for _ in {0..3}; do + tpm_check_failure_with_wrong_pin "$IMAGE" 123457 123456 +done + +# Check LUKS2 token plugin unlock (i.e. without specifying tpm2-device=auto) +if cryptsetup_has_token_plugin_support; then + PIN=123456 systemd-cryptsetup attach test-volume "$IMAGE" - headless=1 + systemd-cryptsetup detach test-volume + + # Check failure with wrong PIN + for _ in {0..3}; do + tpm_check_failure_with_wrong_pin "$IMAGE" 123457 123456 + done +else + echo 'cryptsetup has no LUKS2 token plugin support, skipping' +fi + +# Check failure with wrong PCR (and correct PIN) +tpm2_pcrextend 7:sha256=0000000000000000000000000000000000000000000000000000000000000000 +(! PIN=123456 systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1) + +# Enroll unlock with PCR 0+7 +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +# Check with wrong PCR 0 +tpm2_pcrextend 0:sha256=0000000000000000000000000000000000000000000000000000000000000000 +(! systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1) + +if tpm_has_pcr sha256 12; then + # Enroll using an explicit PCR value (that does match current PCR value) + systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" + EXPECTED_PCR_VALUE=$(cat /sys/class/tpm/tpm0/pcr-sha256/12) + PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="12:sha256=$EXPECTED_PCR_VALUE" "$IMAGE" + systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 + systemd-cryptsetup detach test-volume + + # Same as above plus more PCRs without the value or alg specified + systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" + EXPECTED_PCR_VALUE=$(cat /sys/class/tpm/tpm0/pcr-sha256/12) + PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="1,12:sha256=$EXPECTED_PCR_VALUE,3" "$IMAGE" + systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 + systemd-cryptsetup detach test-volume + + # Same as above plus more PCRs with hash alg specified but hash value not specified + systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" + EXPECTED_PCR_VALUE=$(cat /sys/class/tpm/tpm0/pcr-sha256/12) + PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="1:sha256,12:sha256=$EXPECTED_PCR_VALUE,3" "$IMAGE" + systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 + systemd-cryptsetup detach test-volume + + # Now the interesting part, enrolling using a hash value that doesn't match the current PCR value + systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" + tpm2_pcrread -Q -o /tmp/pcr.dat sha256:12 + CURRENT_PCR_VALUE=$(cat /sys/class/tpm/tpm0/pcr-sha256/12) + EXPECTED_PCR_VALUE=$(cat /tmp/pcr.dat /tmp/pcr.dat | openssl dgst -sha256 -r | cut -d ' ' -f 1) + PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="12:sha256=$EXPECTED_PCR_VALUE" "$IMAGE" + (! systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1) + tpm2_pcrextend "12:sha256=$CURRENT_PCR_VALUE" + systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 + systemd-cryptsetup detach test-volume + + # enroll TPM using device key instead of direct access, then verify unlock using TPM + tpm2_pcrread -Q -o /tmp/pcr.dat sha256:12 + CURRENT_PCR_VALUE=$(cat /sys/class/tpm/tpm0/pcr-sha256/12) + tpm2_readpublic -c 0x81000001 -o /tmp/srk.pub + systemd-analyze srk > /tmp/srk2.pub + cmp /tmp/srk.pub /tmp/srk2.pub + if [ -f /run/systemd/tpm2-srk-public-key.tpm2b_public ] ; then + cmp /tmp/srk.pub /run/systemd/tpm2-srk-public-key.tpm2b_public + fi + + # --tpm2-device-key= requires OpenSSL >= 3 with KDF-SS + if openssl_supports_kdf SSKDF; then + PASSWORD=passphrase systemd-cryptenroll --tpm2-device-key=/tmp/srk.pub --tpm2-pcrs="12:sha256=$CURRENT_PCR_VALUE" "$IMAGE" + systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 + systemd-cryptsetup detach test-volume + fi + + rm -f /tmp/pcr.dat /tmp/srk.pub +fi + +# Use default (0) seal key handle +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0 "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0x0 "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +# Use SRK seal key handle +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=81000001 "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0x81000001 "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +# Test invalid ranges: pcr, nv, session, permanent +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +(! PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=7 "$IMAGE") # PCR +(! PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0x01000001 "$IMAGE") # NV index +(! PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0x02000001 "$IMAGE") # HMAC/loaded session +(! PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0x03000001 "$IMAGE") # Policy/saved session +(! PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle=0x40000001 "$IMAGE") # Permanent + +# Use non-SRK persistent seal key handle (by creating/persisting new key) +PRIMARY=/tmp/primary.ctx +tpm2_createprimary -c "$PRIMARY" +PERSISTENT_LINE=$(tpm2_evictcontrol -c "$PRIMARY" | grep persistent-handle) +PERSISTENT_HANDLE="0x${PERSISTENT_LINE##*0x}" +tpm2_flushcontext -t + +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle="${PERSISTENT_HANDLE#0x}" "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +PASSWORD=passphrase systemd-cryptenroll --tpm2-device=auto --tpm2-seal-key-handle="$PERSISTENT_HANDLE" "$IMAGE" +systemd-cryptsetup attach test-volume "$IMAGE" - tpm2-device=auto,headless=1 +systemd-cryptsetup detach test-volume + +# --tpm2-device-key= requires OpenSSL >= 3 with KDF-SS +if openssl_supports_kdf SSKDF; then + # Make sure that --tpm2-device-key= also works with systemd-repart + tpm2_readpublic -c 0x81000001 -o /tmp/srk.pub + mkdir /tmp/dditest + cat > /tmp/dditest/50-root.conf <<EOF +[Partition] +Type=root +Format=ext4 +CopyFiles=/tmp/dditest:/ +Encrypt=tpm2 +EOF + PASSWORD=passphrase systemd-repart --tpm2-device-key=/tmp/srk.pub --definitions=/tmp/dditest --empty=create --size=50M /tmp/dditest.raw --tpm2-pcrs= + DEVICE="$(systemd-dissect --attach /tmp/dditest.raw)" + systemd-cryptsetup attach dditest "$DEVICE"p1 - tpm2-device=auto,headless=yes + mkdir /tmp/dditest.mnt + mount -t ext4 /dev/mapper/dditest /tmp/dditest.mnt + cmp /tmp/dditest.mnt/50-root.conf /tmp/dditest/50-root.conf + umount /tmp/dditest.mnt + rmdir /tmp/dditest.mnt + rm /tmp/dditest.raw + rm /tmp/dditest/50-root.conf + rmdir /tmp/dditest +fi + +rm -f "$IMAGE" "$PRIMARY" diff --git a/test/units/testsuite-70.measure.sh b/test/units/testsuite-70.measure.sh new file mode 100755 index 0000000..3336f38 --- /dev/null +++ b/test/units/testsuite-70.measure.sh @@ -0,0 +1,130 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug +SD_MEASURE="/usr/lib/systemd/systemd-measure" + +if [[ ! -x "${SD_MEASURE:?}" ]]; then + echo "$SD_MEASURE not found, skipping the test" + exit 0 +fi + +IMAGE="$(mktemp /tmp/systemd-measure-XXX.image)" + +echo HALLO >/tmp/tpmdata1 +echo foobar >/tmp/tpmdata2 + +cat >/tmp/result <<EOF +11:sha1=5177e4ad69db92192c10e5f80402bf81bfec8a81 +11:sha256=37b48bd0b222394dbe3cceff2fca4660c4b0a90ae9369ec90b42f14489989c13 +11:sha384=5573f9b2caf55b1d0a6a701f890662d682af961899f0419cf1e2d5ea4a6a68c1f25bd4f5b8a0865eeee82af90f5cb087 +11:sha512=961305d7e9981d6606d1ce97b3a9a1f92610cac033e9c39064895f0e306abc1680463d55767bd98e751eae115bdef3675a9ee1d29ed37da7885b1db45bb2555b +EOF +"$SD_MEASURE" calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=: | cmp - /tmp/result + +cat >/tmp/result.json <<EOF +{"sha1":[{"pcr":11,"hash":"5177e4ad69db92192c10e5f80402bf81bfec8a81"}],"sha256":[{"pcr":11,"hash":"37b48bd0b222394dbe3cceff2fca4660c4b0a90ae9369ec90b42f14489989c13"}],"sha384":[{"pcr":11,"hash":"5573f9b2caf55b1d0a6a701f890662d682af961899f0419cf1e2d5ea4a6a68c1f25bd4f5b8a0865eeee82af90f5cb087"}],"sha512":[{"pcr":11,"hash":"961305d7e9981d6606d1ce97b3a9a1f92610cac033e9c39064895f0e306abc1680463d55767bd98e751eae115bdef3675a9ee1d29ed37da7885b1db45bb2555b"}]} +EOF +"$SD_MEASURE" calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=: -j | diff -u - /tmp/result.json + +cat >/tmp/result <<EOF +11:sha1=6765ee305db063040c454d32697d922b3d4f232b +11:sha256=21c49c1242042649e09c156546fd7d425ccc3c67359f840507b30be4e0f6f699 +11:sha384=08d0b003a134878eee552070d51d58abe942f457ca85704131dd36f73728e7327ca837594bc9d5ac7de818d02a3d5dd2 +11:sha512=65120f6ebc04b156421c6f3d543b2fad545363d9ca61c514205459e9c0e0b22e09c23605eae5853e38458ef3ca54e087168af8d8a882a98d220d9391e48be6d0 +EOF +"$SD_MEASURE" calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=foo | cmp - /tmp/result + +cat >/tmp/result.json <<EOF +{"sha1":[{"phase":"foo","pcr":11,"hash":"6765ee305db063040c454d32697d922b3d4f232b"}],"sha256":[{"phase":"foo","pcr":11,"hash":"21c49c1242042649e09c156546fd7d425ccc3c67359f840507b30be4e0f6f699"}],"sha384":[{"phase":"foo","pcr":11,"hash":"08d0b003a134878eee552070d51d58abe942f457ca85704131dd36f73728e7327ca837594bc9d5ac7de818d02a3d5dd2"}],"sha512":[{"phase":"foo","pcr":11,"hash":"65120f6ebc04b156421c6f3d543b2fad545363d9ca61c514205459e9c0e0b22e09c23605eae5853e38458ef3ca54e087168af8d8a882a98d220d9391e48be6d0"}]} +EOF +"$SD_MEASURE" calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=foo -j | diff -u - /tmp/result.json + +rm /tmp/result /tmp/result.json + +if ! tpm_has_pcr sha1 11 || ! tpm_has_pcr sha256 11; then + echo "PCR sysfs files not found, skipping signed PCR policy tests" + exit 0 +fi + +# Generate key pair +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "/tmp/pcrsign-private.pem" +openssl rsa -pubout -in "/tmp/pcrsign-private.pem" -out "/tmp/pcrsign-public.pem" + +MEASURE_BANKS=("--bank=sha256") +# Check if SHA1 signatures are supported +# +# Some distros have started phasing out SHA1, so make sure the SHA1 +# signatures are supported before trying to use them. +if echo hello | openssl dgst -sign /tmp/pcrsign-private.pem -sha1 >/dev/null; then + MEASURE_BANKS+=("--bank=sha1") +fi + +# Sign current PCR state with it +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: | tee "/tmp/pcrsign.sig" +dd if=/dev/urandom of=/tmp/pcrtestdata bs=1024 count=64 +systemd-creds encrypt /tmp/pcrtestdata /tmp/pcrtestdata.encrypted --with-key=host+tpm2-with-public-key --tpm2-public-key="/tmp/pcrsign-public.pem" +systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig" | cmp - /tmp/pcrtestdata + +# Invalidate PCR, decrypting should fail now +tpm2_pcrextend 11:sha256=0000000000000000000000000000000000000000000000000000000000000000 +(! systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig" >/dev/null) + +# Sign new PCR state, decrypting should work now. +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: >"/tmp/pcrsign.sig2" +systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig2" | cmp - /tmp/pcrtestdata + +# Now, do the same, but with a cryptsetup binding +truncate -s 20M "$IMAGE" +cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$IMAGE" /tmp/passphrase +# Ensure that an unrelated signature, when not requested, is not used +touch /run/systemd/tpm2-pcr-signature.json +systemd-cryptenroll --unlock-key-file=/tmp/passphrase --tpm2-device=auto --tpm2-public-key="/tmp/pcrsign-public.pem" "$IMAGE" +# Reset and use the signature now +rm -f /run/systemd/tpm2-pcr-signature.json +systemd-cryptenroll --wipe-slot=tpm2 "$IMAGE" +systemd-cryptenroll --unlock-key-file=/tmp/passphrase --tpm2-device=auto --tpm2-public-key="/tmp/pcrsign-public.pem" --tpm2-signature="/tmp/pcrsign.sig2" "$IMAGE" + +# Check if we can activate that (without the token module stuff) +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig2",headless=1 +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 systemd-cryptsetup detach test-volume2 + +# Check if we can activate that (and a second time with the token module stuff enabled) +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig2",headless=1 +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 systemd-cryptsetup detach test-volume2 + +# After extending the PCR things should fail +tpm2_pcrextend 11:sha256=0000000000000000000000000000000000000000000000000000000000000000 +(! SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig2",headless=1) +(! SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig2",headless=1) + +# But once we sign the current PCRs, we should be able to unlock again +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: >"/tmp/pcrsign.sig3" +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig3",headless=1 +systemd-cryptsetup detach test-volume2 +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig3",headless=1 +systemd-cryptsetup detach test-volume2 + +# Test --append mode and de-duplication. With the same parameters signing should not add a new entry +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: --append="/tmp/pcrsign.sig3" >"/tmp/pcrsign.sig4" +cmp "/tmp/pcrsign.sig3" "/tmp/pcrsign.sig4" + +# Sign one more phase, this should +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=quux:waldo --append="/tmp/pcrsign.sig4" >"/tmp/pcrsign.sig5" +(! cmp "/tmp/pcrsign.sig4" "/tmp/pcrsign.sig5") + +# Should still be good to unlock, given the old entry still exists +SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 systemd-cryptsetup attach test-volume2 "$IMAGE" - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig5",headless=1 +systemd-cryptsetup detach test-volume2 + +# Adding both signatures once more should not change anything, due to the deduplication +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: --append="/tmp/pcrsign.sig5" >"/tmp/pcrsign.sig6" +"$SD_MEASURE" sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=quux:waldo --append="/tmp/pcrsign.sig6" >"/tmp/pcrsign.sig7" +cmp "/tmp/pcrsign.sig5" "/tmp/pcrsign.sig7" + +rm -f "$IMAGE" diff --git a/test/units/testsuite-70.pcrextend.sh b/test/units/testsuite-70.pcrextend.sh new file mode 100755 index 0000000..318fce0 --- /dev/null +++ b/test/units/testsuite-70.pcrextend.sh @@ -0,0 +1,124 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug +SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend" + +if [[ ! -x "${SD_PCREXTEND:?}" ]] || ! tpm_has_pcr sha256 11 || ! tpm_has_pcr sha256 15; then + echo "$SD_PCREXTEND or PCR sysfs files not found, skipping PCR extension tests" + exit 0 +fi + +at_exit() { + if [[ $? -ne 0 ]]; then + # Dump the event log on fail, to make debugging a bit easier + jq --seq --slurp </run/log/systemd/tpm2-measure.log + fi +} + +trap at_exit EXIT + +# Temporarily override sd-pcrextend's sanity checks +export SYSTEMD_FORCE_MEASURE=1 + +"$SD_PCREXTEND" --help +"$SD_PCREXTEND" --version +"$SD_PCREXTEND" foo +"$SD_PCREXTEND" --machine-id +"$SD_PCREXTEND" --tpm2-device=list +"$SD_PCREXTEND" --tpm2-device=auto foo +"$SD_PCREXTEND" --tpm2-device=/dev/tpm0 foo +"$SD_PCREXTEND" --bank=sha256 foo +"$SD_PCREXTEND" --bank=sha256 --bank=sha256 foo +"$SD_PCREXTEND" --graceful foo +"$SD_PCREXTEND" --pcr=15 foo +"$SD_PCREXTEND" --file-system=/ +"$SD_PCREXTEND" --file-system=/tmp --file-system=/ +"$SD_PCREXTEND" --file-system=/tmp --file-system=/ --pcr=15 --pcr=11 + +if tpm_has_pcr sha1 11; then + "$SD_PCREXTEND" --bank=sha1 --pcr=11 foo +fi + +(! "$SD_PCREXTEND") +(! "$SD_PCREXTEND" "") +(! "$SD_PCREXTEND" foo bar) +(! "$SD_PCREXTEND" --bank= foo) +(! "$SD_PCREXTEND" --tpm2-device= foo) +(! "$SD_PCREXTEND" --tpm2-device=/dev/null foo) +(! "$SD_PCREXTEND" --pcr= foo) +(! "$SD_PCREXTEND" --pcr=-1 foo) +(! "$SD_PCREXTEND" --pcr=1024 foo) +(! "$SD_PCREXTEND" --foo=bar) + +unset SYSTEMD_FORCE_MEASURE + +# Note: since we're reading the TPM event log as json-seq, the same rules apply to the output +# as well, i.e. each record is prefixed by RS (0x1E, 036) and suffixed by LF (0x0A, 012). +# LF is usually eaten by bash, but RS needs special handling. + +# Save the number of events in the current event log, so we can skip them when +# checking changes caused by following tests +RECORD_COUNT="$(jq --seq --slurp '. | length' </run/log/systemd/tpm2-measure.log | tr -d '\036')" + +# Let's measure the machine ID +tpm2_pcrread sha256:15 -Q -o /tmp/oldpcr15 +mv /etc/machine-id /etc/machine-id.save +echo 994013bf23864ee7992eab39a96dd3bb >/etc/machine-id +SYSTEMD_FORCE_MEASURE=1 "$SD_PCREXTEND" --machine-id +mv /etc/machine-id.save /etc/machine-id +tpm2_pcrread sha256:15 -Q -o /tmp/newpcr15 + +# And check it matches expectations +diff /tmp/newpcr15 \ + <(cat /tmp/oldpcr15 <(echo -n "machine-id:994013bf23864ee7992eab39a96dd3bb" | openssl dgst -binary -sha256) | openssl dgst -binary -sha256) + +# Check that the event log record was properly written +test "$(jq --seq --slurp ".[$RECORD_COUNT].pcr" </run/log/systemd/tpm2-measure.log)" == "$(printf '\x1e15')" +DIGEST_EXPECTED="$(echo -n "machine-id:994013bf23864ee7992eab39a96dd3bb" | openssl dgst -hex -sha256 -r)" +DIGEST_CURRENT="$(jq --seq --slurp --raw-output ".[$RECORD_COUNT].digests[] | select(.hashAlg == \"sha256\").digest" </run/log/systemd/tpm2-measure.log) *stdin" +test "$DIGEST_EXPECTED" == "$DIGEST_CURRENT" + +RECORD_COUNT=$((RECORD_COUNT + 1)) +# And similar for the boot phase measurement into PCR 11 +tpm2_pcrread sha256:11 -Q -o /tmp/oldpcr11 +# Do the equivalent of 'SYSTEMD_FORCE_MEASURE=1 "$SD_PCREXTEND" foobar' via Varlink, just to test the Varlink logic (but first we need to patch out the conditionalization...) +mkdir -p /run/systemd/system/systemd-pcrextend.socket.d +cat > /run/systemd/system/systemd-pcrextend.socket.d/50-no-condition.conf <<EOF +[Unit] +# Turn off all conditions */ +ConditionSecurity= +EOF +systemctl daemon-reload +systemctl restart systemd-pcrextend.socket +varlinkctl call /run/systemd/io.systemd.PCRExtend io.systemd.PCRExtend.Extend '{"pcr":11,"text":"foobar"}' +tpm2_pcrread sha256:11 -Q -o /tmp/newpcr11 + +diff /tmp/newpcr11 \ + <(cat /tmp/oldpcr11 <(echo -n "foobar" | openssl dgst -binary -sha256) | openssl dgst -binary -sha256) + +# Check the event log for the 2nd new record since $RECORD_COUNT +test "$(jq --seq --slurp ".[$RECORD_COUNT].pcr" </run/log/systemd/tpm2-measure.log)" == "$(printf '\x1e11')" +DIGEST_EXPECTED="$(echo -n "foobar" | openssl dgst -hex -sha256 -r)" +DIGEST_CURRENT="$(jq --seq --slurp --raw-output ".[$RECORD_COUNT].digests[] | select(.hashAlg == \"sha256\").digest" </run/log/systemd/tpm2-measure.log) *stdin" +test "$DIGEST_EXPECTED" == "$DIGEST_CURRENT" + +# Measure a file system into PCR 15 +tpm2_pcrread sha256:15 -Q -o /tmp/oldpcr15 +SYSTEMD_FORCE_MEASURE=1 "$SD_PCREXTEND" --file-system=/ +# Put together the "file system word" we just sent to the TPM +# file-system:MOUNTPOINT:TYPE:UUID:LABEL:PART_ENTRY_UUID:PART_ENTRY_TYPE:PART_ENTRY_NAME +ROOT_DEVICE="$(findmnt -n -o SOURCE /)" +FS_WORD="$(lsblk -n -o MOUNTPOINT,FSTYPE,UUID,LABEL,PARTUUID,PARTTYPE,PARTLABEL "$ROOT_DEVICE" | sed -r 's/[ ]+/:/g')" +tpm2_pcrread sha256:15 -Q -o /tmp/newpcr15 + +# And check if it matches with the current PCR 15 state +diff /tmp/newpcr15 \ + <(cat /tmp/oldpcr15 <(echo -n "file-system:$FS_WORD" | openssl dgst -binary -sha256) | openssl dgst -binary -sha256) + +rm -f /tmp/oldpcr{11,15} /tmp/newpcr{11,15} diff --git a/test/units/testsuite-70.pcrlock.sh b/test/units/testsuite-70.pcrlock.sh new file mode 100755 index 0000000..3da9926 --- /dev/null +++ b/test/units/testsuite-70.pcrlock.sh @@ -0,0 +1,146 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug +export PAGER= +SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend" +SD_PCRLOCK="/usr/lib/systemd/systemd-pcrlock" + +if [[ ! -x "${SD_PCREXTEND:?}" ]] || [[ ! -x "${SD_PCRLOCK:?}" ]] ; then + echo "$SD_PCREXTEND or $SD_PCRLOCK not found, skipping pcrlock tests" + exit 0 +fi + +at_exit() { + if [[ $? -ne 0 ]]; then + # Dump the event log on fail, to make debugging a bit easier + [[ -e /run/log/systemd/tpm2-measure.log ]] && jq --seq --slurp </run/log/systemd/tpm2-measure.log + fi + + return 0 +} + +trap at_exit EXIT + +# Temporarily override sd-pcrextend's sanity checks +export SYSTEMD_FORCE_MEASURE=1 + +# The PCRs we are going to lock to. We exclude the various PCRs we touched +# above where no event log record was written, because we cannot analyze +# things without event log. We include debug PCR 16, see below. +PCRS="1+2+3+4+5+16" + +# Remove the old measurement log, as it contains all kinds of nonsense from the +# previous test, which will fail our consistency checks. Removing the file also +# means we'll fail consistency check, but at least we'll fail them consistently +# (as the PCR values simply won't match the log). +rm -f /run/log/systemd/tpm2-measure.log + +# Ensure a truncated log doesn't crash pcrlock +echo -n -e \\x1e >/tmp/borked +set +e +SYSTEMD_MEASURE_LOG_USERSPACE=/tmp/borked "$SD_PCRLOCK" cel --no-pager --json=pretty +ret=$? +set -e +# If it crashes the exit code will be 149 +test $ret -eq 1 + +SYSTEMD_COLORS=256 "$SD_PCRLOCK" +"$SD_PCRLOCK" cel --no-pager --json=pretty +"$SD_PCRLOCK" log --pcr="$PCRS" +"$SD_PCRLOCK" log --json=pretty --pcr="$PCRS" +"$SD_PCRLOCK" list-components +"$SD_PCRLOCK" list-components --location=250- +"$SD_PCRLOCK" list-components --location=250-:350- +"$SD_PCRLOCK" lock-firmware-config +"$SD_PCRLOCK" lock-gpt +"$SD_PCRLOCK" lock-machine-id +"$SD_PCRLOCK" lock-file-system +"$SD_PCRLOCK" lock-file-system / +"$SD_PCRLOCK" predict --pcr="$PCRS" +"$SD_PCRLOCK" predict --pcr="0x1+0x3+4" +"$SD_PCRLOCK" predict --json=pretty --pcr="$PCRS" + +SD_STUB="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)" +if [[ -n "$SD_STUB" ]]; then + "$SD_PCRLOCK" lock-pe "$SD_STUB" + "$SD_PCRLOCK" lock-pe <"$SD_STUB" + "$SD_PCRLOCK" lock-uki "$SD_STUB" + "$SD_PCRLOCK" lock-uki <"$SD_STUB" +fi + +PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=yes +# Repeat immediately (this call will have to reuse the nvindex, rather than create it) +"$SD_PCRLOCK" make-policy --pcr="$PCRS" +"$SD_PCRLOCK" make-policy --pcr="$PCRS" --force + +img="/tmp/pcrlock.img" +truncate -s 20M "$img" +echo -n hoho >/tmp/pcrlockpwd +chmod 0600 /tmp/pcrlockpwd +cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$img" /tmp/pcrlockpwd + +systemd-cryptenroll --unlock-key-file=/tmp/pcrlockpwd --tpm2-device=auto --tpm2-pcrlock=/var/lib/systemd/pcrlock.json --tpm2-public-key= --wipe-slot=tpm2 "$img" +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless +systemd-cryptsetup detach pcrlock + +# Measure something into PCR 16 (the "debug" PCR), which should make the activation fail +"$SD_PCREXTEND" --pcr=16 test70 + +"$SD_PCRLOCK" cel --json=pretty + +(! systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless ) + +# Now add a component for it, rebuild policy and it should work (we'll rebuild +# once like that, but don't provide the recovery pin. This should fail, since +# the PCR is hosed after all. But then we'll use recovery pin, and it should +# work. +echo -n test70 | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/910-test70.pcrlock --pcr=16 +(! "$SD_PCRLOCK" make-policy --pcr="$PCRS") +PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=yes + +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless +systemd-cryptsetup detach pcrlock + +# And now let's do it the clean way, and generate the right policy ahead of time. +echo -n test70-take-two | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/920-test70.pcrlock --pcr=16 +"$SD_PCRLOCK" make-policy --pcr="$PCRS" + +"$SD_PCREXTEND" --pcr=16 test70-take-two + +"$SD_PCRLOCK" cel --json=pretty + +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless +systemd-cryptsetup detach pcrlock + +"$SD_PCRLOCK" remove-policy + +"$SD_PCRLOCK" unlock-firmware-config +"$SD_PCRLOCK" unlock-gpt +"$SD_PCRLOCK" unlock-machine-id +"$SD_PCRLOCK" unlock-file-system +"$SD_PCRLOCK" unlock-raw --pcrlock=/var/lib/pcrlock.d/910-test70.pcrlock +"$SD_PCRLOCK" unlock-raw --pcrlock=/var/lib/pcrlock.d/920-test70.pcrlock + +(! "$SD_PCRLOCK" "") +(! "$SD_PCRLOCK" predict --pcr=-1) +(! "$SD_PCRLOCK" predict --pcr=foo) +(! "$SD_PCRLOCK" predict --pcr=1+1) +(! "$SD_PCRLOCK" predict --pcr=1+++++1) +(! "$SD_PCRLOCK" make-policy --nv-index=0) +(! "$SD_PCRLOCK" make-policy --nv-index=foo) +(! "$SD_PCRLOCK" list-components --location=:) +(! "$SD_PCRLOCK" lock-gpt "") +(! "$SD_PCRLOCK" lock-gpt /dev/sr0) +(! "$SD_PCRLOCK" lock-pe /dev/full) +(! "$SD_PCRLOCK" lock-pe /bin/true) +(! "$SD_PCRLOCK" lock-uki /dev/full) +(! "$SD_PCRLOCK" lock-uki /bin/true) +(! "$SD_PCRLOCK" lock-file-system "") + +rm "$img" /tmp/pcrlockpwd diff --git a/test/units/testsuite-70.service b/test/units/testsuite-70.service new file mode 100644 index 0000000..c13c2d5 --- /dev/null +++ b/test/units/testsuite-70.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-70-TPM2 + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh diff --git a/test/units/testsuite-70.sh b/test/units/testsuite-70.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-70.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-70.tpm2-setup.sh b/test/units/testsuite-70.tpm2-setup.sh new file mode 100755 index 0000000..faf6fe7 --- /dev/null +++ b/test/units/testsuite-70.tpm2-setup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +export SYSTEMD_LOG_LEVEL=debug +SD_TPM2SETUP="/usr/lib/systemd/systemd-tpm2-setup" + +if [[ ! -x "${SD_TPM2SETUP:?}" ]]; then + echo "$SD_TPM2SETUP not found, skipping the test" + exit 0 +fi + +"$SD_TPM2SETUP" --help +"$SD_TPM2SETUP" --version +"$SD_TPM2SETUP" --tpm2-device=list +"$SD_TPM2SETUP" --tpm2-device=auto +"$SD_TPM2SETUP" --tpm2-device=/dev/tpm0 +"$SD_TPM2SETUP" --early=yes +"$SD_TPM2SETUP" --early=yes +"$SD_TPM2SETUP" --early=no +"$SD_TPM2SETUP" --early=no + +(! "$SD_TPM2SETUP" "") +(! "$SD_TPM2SETUP" --tpm2-device=) +(! "$SD_TPM2SETUP" --tpm2-device=/dev/null) +(! "$SD_TPM2SETUP" --foo=bar) diff --git a/test/units/testsuite-71.service b/test/units/testsuite-71.service new file mode 100644 index 0000000..1718629 --- /dev/null +++ b/test/units/testsuite-71.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-71-HOSTNAME + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-71.sh b/test/units/testsuite-71.sh new file mode 100755 index 0000000..da765a9 --- /dev/null +++ b/test/units/testsuite-71.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +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 + +restore_hostname() { + if [[ -e /tmp/hostname.bak ]]; then + mv /tmp/hostname.bak /etc/hostname + else + rm -f /etc/hostname + fi +} + +testcase_hostname() { + local orig= + + if [[ -f /etc/hostname ]]; then + cp /etc/hostname /tmp/hostname.bak + orig=$(cat /etc/hostname) + fi + + trap restore_hostname RETURN + + # should activate daemon and work + if [[ -n "$orig" ]]; then + assert_in "Static hostname: $orig" "$(hostnamectl)" + fi + assert_in "Kernel: $(uname -s) $(uname -r)" "$(hostnamectl)" + + # change hostname + assert_rc 0 hostnamectl set-hostname testhost + assert_eq "$(cat /etc/hostname)" "testhost" + assert_in "Static hostname: testhost" "$(hostnamectl)" + + if [[ -n "$orig" ]]; then + # reset to original + assert_rc 0 hostnamectl set-hostname "$orig" + assert_eq "$(cat /etc/hostname)" "$orig" + assert_in "Static hostname: $orig" "$(hostnamectl)" + fi +} + +restore_machine_info() { + if [[ -e /tmp/machine-info.bak ]]; then + mv /tmp/machine-info.bak /etc/machine-info + else + rm -f /etc/machine-info + fi +} + +get_chassis() ( + # shellcheck source=/dev/null + . /etc/machine-info + + echo "$CHASSIS" +) + +testcase_chassis() { + local i + + if [[ -f /etc/machine-info ]]; then + cp /etc/machine-info /tmp/machine-info.bak + fi + + trap restore_machine_info RETURN + + # Invalid chassis type is refused + assert_rc 1 hostnamectl chassis hoge + + # Valid chassis types + for i in vm container desktop laptop convertible server tablet handset watch embedded; do + hostnamectl chassis "$i" + assert_eq "$(hostnamectl chassis)" "$i" + assert_eq "$(get_chassis)" "$i" + done + + systemctl stop systemd-hostnamed.service + rm -f /etc/machine-info + + # fallback chassis type + if systemd-detect-virt --quiet --container; then + assert_eq "$(hostnamectl chassis)" container + elif systemd-detect-virt --quiet --vm; then + assert_eq "$(hostnamectl chassis)" vm + fi +} + +restore_sysfs_dmi() { + umount /sys/class/dmi/id + rm -rf /run/systemd/system/systemd-hostnamed.service.d + systemctl daemon-reload + systemctl stop systemd-hostnamed +} + +testcase_firmware_date() { + # No DMI on s390x or ppc + if [[ ! -d /sys/class/dmi/id ]]; then + echo "/sys/class/dmi/id not found, skipping firmware date tests." + return 0 + fi + + trap restore_sysfs_dmi RETURN + + # Ignore /sys being mounted as tmpfs + mkdir -p /run/systemd/system/systemd-hostnamed.service.d/ + cat >/run/systemd/system/systemd-hostnamed.service.d/override.conf <<EOF +[Service] +Environment="SYSTEMD_DEVICE_VERIFY_SYSFS=0" +Environment="SYSTEMD_HOSTNAME_FORCE_DMI=1" +EOF + systemctl daemon-reload + + mount -t tmpfs none /sys/class/dmi/id + echo '1' >/sys/class/dmi/id/uevent + + echo '09/08/2000' >/sys/class/dmi/id/bios_date + systemctl stop systemd-hostnamed + assert_in '2000-09-08' "$(hostnamectl)" + + echo '2022' >/sys/class/dmi/id/bios_date + systemctl stop systemd-hostnamed + assert_not_in 'Firmware Date' "$(hostnamectl)" + + echo 'garbage' >/sys/class/dmi/id/bios_date + systemctl stop systemd-hostnamed + assert_not_in 'Firmware Date' "$(hostnamectl)" +} + +testcase_nss-myhostname() { + local database host i + + HOSTNAME="$(hostnamectl hostname)" + + # Set up a dummy network for _gateway and _outbound labels + ip link add foo type dummy + ip link set up dev foo + ip addr add 10.0.0.2/24 dev foo + for i in {128..150}; do + ip addr add "10.0.0.$i/24" dev foo + done + ip route add 10.0.0.1 dev foo + ip route add default via 10.0.0.1 dev foo + + # Note: `getent hosts` probes gethostbyname2(), whereas `getent ahosts` probes gethostbyname3() + # and gethostbyname4() (through getaddrinfo() -> gaih_inet() -> get_nss_addresses()) + getent hosts -s myhostname + getent ahosts -s myhostname + + # With IPv6 disabled + sysctl -w net.ipv6.conf.all.disable_ipv6=1 + # Everything under .localhost and .localhost.localdomain should resolve to localhost + for host in {foo.,foo.bar.baz.,.,}localhost{,.} {foo.,foo.bar.baz.,.,}localhost.localdomain{,.}; do + run_and_grep "^127\.0\.0\.1\s+localhost$" getent hosts -s myhostname "$host" + run_and_grep "^127\.0\.0\.1\s+STREAM\s+localhost" getent ahosts -s myhostname "$host" + run_and_grep "^127\.0\.0\.1\s+STREAM\s+localhost" getent ahostsv4 -s myhostname "$host" + (! getent ahostsv6 -s myhostname localhost) + done + for i in 2 {128..150}; do + run_and_grep "^10\.0\.0\.$i\s+$HOSTNAME$" getent hosts -s myhostname "$HOSTNAME" + run_and_grep "^10\.0\.0\.$i\s+" getent ahosts -s myhostname "$HOSTNAME" + run_and_grep "^10\.0\.0\.$i\s+" getent ahostsv4 -s myhostname "$HOSTNAME" + run_and_grep "^10\.0\.0\.$i\s+$HOSTNAME$" getent hosts -s myhostname "10.0.0.$i" + run_and_grep "^10\.0\.0\.$i\s+STREAM\s+10\.0\.0\.$i$" getent ahosts -s myhostname "10.0.0.$i" + run_and_grep "^10\.0\.0\.$i\s+STREAM\s+10\.0\.0\.$i$" getent ahostsv4 -s myhostname "10.0.0.$i" + done + for database in hosts ahosts ahostsv4 ahostsv6; do + (! getent "$database" -s myhostname ::1) + done + (! getent ahostsv6 -s myhostname "$HOSTNAME") + run_and_grep -n "^fe80:[^ ]+\s+STREAM$" getent ahosts -s myhostname "$HOSTNAME" + + # With IPv6 enabled + sysctl -w net.ipv6.conf.all.disable_ipv6=0 + # Everything under .localhost and .localhost.localdomain should resolve to localhost + for host in {foo.,foo.bar.baz.,.,}localhost{,.} {foo.,foo.bar.baz.,.,}localhost.localdomain{,.}; do + run_and_grep "^::1\s+localhost$" getent hosts -s myhostname "$host" + run_and_grep "^::1\s+STREAM" getent ahosts -s myhostname "$host" + run_and_grep "^127\.0\.0\.1\s+STREAM" getent ahosts -s myhostname "$host" + run_and_grep "^127\.0\.0\.1\s+STREAM" getent ahostsv4 -s myhostname "$host" + run_and_grep -n "^::1\s+STREAM" getent ahostsv4 -s myhostname "$host" + run_and_grep "^::1\s+STREAM" getent ahostsv6 -s myhostname "$host" + run_and_grep -n "^127\.0\.0\.1\s+STREAM" getent ahostsv6 -s myhostname "$host" + done + for i in 2 {128..150}; do + run_and_grep "^10\.0\.0\.$i\s+" getent ahosts -s myhostname "$HOSTNAME" + run_and_grep "^10\.0\.0\.$i\s+" getent ahostsv4 -s myhostname "$HOSTNAME" + run_and_grep "^10\.0\.0\.$i\s+STREAM\s+10\.0\.0\.$i$" getent ahosts -s myhostname "10.0.0.$i" + run_and_grep "^10\.0\.0\.$i\s+STREAM\s+10\.0\.0\.$i$" getent ahostsv4 -s myhostname "10.0.0.$i" + done + run_and_grep "^fe80:[^ ]+\s+$HOSTNAME$" getent hosts -s myhostname "$HOSTNAME" + run_and_grep "^fe80:[^ ]+\s+STREAM" getent ahosts -s myhostname "$HOSTNAME" + run_and_grep "^127\.0\.0\.1\s+localhost$" getent hosts -s myhostname 127.0.0.1 + run_and_grep "^127\.0\.0\.1\s+STREAM\s+127\.0\.0\.1$" getent ahosts -s myhostname 127.0.0.1 + run_and_grep "^::ffff:127\.0\.0\.1\s+STREAM\s+127\.0\.0\.1$" getent ahostsv6 -s myhostname 127.0.0.1 + run_and_grep "^127\.0\.0\.2\s+$HOSTNAME$" getent hosts -s myhostname 127.0.0.2 + run_and_grep "^::1\s+localhost $HOSTNAME$" getent hosts -s myhostname ::1 + run_and_grep "^::1\s+STREAM\s+::1$" getent ahosts -s myhostname ::1 + (! getent ahostsv4 -s myhostname ::1) + + # _gateway + for host in _gateway{,.} 10.0.0.1; do + run_and_grep "^10\.0\.0\.1\s+_gateway$" getent hosts -s myhostname "$host" + run_and_grep "^10\.0\.0\.1\s+STREAM" getent ahosts -s myhostname "$host" + done + + # _outbound + for host in _outbound{,.} 10.0.0.2; do + run_and_grep "^10\.0\.0\.2\s+" getent hosts -s myhostname "$host" + run_and_grep "^10\.0\.0\.2\s+STREAM" getent ahosts -s myhostname "$host" + done + + # Non-existent records + for database in hosts ahosts ahostsv4 ahostsv6; do + (! getent "$database" -s myhostname this.should.not.exist) + done + (! getent hosts -s myhostname 10.254.254.1) + (! getent hosts -s myhostname fd00:dead:beef:cafe::1) +} + +run_testcases + +touch /testok diff --git a/test/units/testsuite-72.service b/test/units/testsuite-72.service new file mode 100644 index 0000000..1640350 --- /dev/null +++ b/test/units/testsuite-72.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-72-SYSUPDATE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-72.sh b/test/units/testsuite-72.sh new file mode 100755 index 0000000..953f2a1 --- /dev/null +++ b/test/units/testsuite-72.sh @@ -0,0 +1,278 @@ +#!/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 +set -eux +set -o pipefail + +SYSUPDATE=/lib/systemd/systemd-sysupdate +SECTOR_SIZES="512 4096" +BACKING_FILE=/var/tmp/72-joined.raw +export SYSTEMD_ESP_PATH=/var/tmp/72-esp +export SYSTEMD_XBOOTLDR_PATH=/var/tmp/72-xbootldr +export SYSTEMD_PAGER=cat +export SYSTEMD_LOG_LEVEL=debug + +if ! test -x "$SYSUPDATE"; then + echo "no systemd-sysupdate" >/skipped + exit 0 +fi + +# Loopback devices may not be supported. They are used because sfdisk cannot +# change the sector size of a file, and we want to test both 512 and 4096 byte +# sectors. If loopback devices are not supported, we can only test one sector +# size, and the underlying device is likely to have a sector size of 512 bytes. +if ! losetup --find >/dev/null 2>&1; then + echo "No loopback device support" + SECTOR_SIZES="512" +fi + +trap cleanup ERR +cleanup() { + set +o pipefail + blockdev="$( losetup --list --output NAME,BACK-FILE | grep $BACKING_FILE | cut -d' ' -f1)" + [ -n "$blockdev" ] && losetup --detach "$blockdev" + rm -f "$BACKING_FILE" + rm -rf /var/tmp/72-{dirs,defs,source,xbootldr,esp} + rm -f /testok +} + +new_version() { + # Inputs: + # $1: sector size + # $2: version + + # Create a pair of random partition payloads, and compress one + dd if=/dev/urandom of="/var/tmp/72-source/part1-$2.raw" bs="$1" count=2048 + dd if=/dev/urandom of="/var/tmp/72-source/part2-$2.raw" bs="$1" count=2048 + gzip -k -f "/var/tmp/72-source/part2-$2.raw" + + # Create a random "UKI" payload + echo $RANDOM >"/var/tmp/72-source/uki-$2.efi" + + # Create a random extra payload + echo $RANDOM >"/var/tmp/72-source/uki-extra-$2.efi" + + # Create tarball of a directory + mkdir -p "/var/tmp/72-source/dir-$2" + echo $RANDOM >"/var/tmp/72-source/dir-$2/foo.txt" + echo $RANDOM >"/var/tmp/72-source/dir-$2/bar.txt" + tar --numeric-owner -C "/var/tmp/72-source/dir-$2/" -czf "/var/tmp/72-source/dir-$2.tar.gz" . + + ( cd /var/tmp/72-source/ && sha256sum uki* part* dir-*.tar.gz >SHA256SUMS ) +} + +update_now() { + # Update to newest version. First there should be an update ready, then we + # do the update, and then there should not be any ready anymore + + "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no check-new + "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no update + ( ! "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no check-new ) +} + +verify_version() { + # Inputs: + # $1: block device + # $2: sector size + # $3: version + # $4: partition number of part1 + # $5: partition number of part2 + + gpt_reserved_sectors=$(( 1024 * 1024 / $2 )) + part1_offset=$(( ( $4 - 1 ) * 2048 + gpt_reserved_sectors )) + part2_offset=$(( ( $5 - 1 ) * 2048 + gpt_reserved_sectors )) + + # Check the partitions + dd if="$1" bs="$2" skip="$part1_offset" count=2048 | cmp "/var/tmp/72-source/part1-$3.raw" + dd if="$1" bs="$2" skip="$part2_offset" count=2048 | cmp "/var/tmp/72-source/part2-$3.raw" + + # Check the UKI + cmp "/var/tmp/72-source/uki-$3.efi" "/var/tmp/72-xbootldr/EFI/Linux/uki_$3+3-0.efi" + test -z "$(ls -A /var/tmp/72-esp/EFI/Linux)" + + # Check the extra efi + cmp "/var/tmp/72-source/uki-extra-$3.efi" "/var/tmp/72-xbootldr/EFI/Linux/uki_$3.efi.extra.d/extra.addon.efi" + + # Check the directories + cmp "/var/tmp/72-source/dir-$3/foo.txt" /var/tmp/72-dirs/current/foo.txt + cmp "/var/tmp/72-source/dir-$3/bar.txt" /var/tmp/72-dirs/current/bar.txt +} + +for sector_size in $SECTOR_SIZES ; do + # Disk size of: + # - 1MB for GPT + # - 4 partitions of 2048 sectors each + # - 1MB for backup GPT + disk_size=$(( sector_size * 2048 * 4 + 1024 * 1024 * 2 )) + rm -f "$BACKING_FILE" + truncate -s "$disk_size" "$BACKING_FILE" + + if losetup --find >/dev/null 2>&1; then + # shellcheck disable=SC2086 + blockdev="$(losetup --find --show --sector-size $sector_size $BACKING_FILE)" + else + blockdev="$BACKING_FILE" + fi + + sfdisk "$blockdev" <<EOF +label: gpt +unit: sectors +sector-size: $sector_size + +size=2048, type=4f68bce3-e8cd-4db1-96e7-fbcaf984b709, name=_empty +size=2048, type=4f68bce3-e8cd-4db1-96e7-fbcaf984b709, name=_empty +size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty +size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty +EOF + + rm -rf /var/tmp/72-dirs + mkdir -p /var/tmp/72-dirs + + rm -rf /var/tmp/72-defs + mkdir -p /var/tmp/72-defs + + cat >/var/tmp/72-defs/01-first.conf <<EOF +[Source] +Type=regular-file +Path=/var/tmp/72-source +MatchPattern=part1-@v.raw + +[Target] +Type=partition +Path=$blockdev +MatchPattern=part1-@v +MatchPartitionType=root-x86-64 +EOF + + cat >/var/tmp/72-defs/02-second.conf <<EOF +[Source] +Type=regular-file +Path=/var/tmp/72-source +MatchPattern=part2-@v.raw.gz + +[Target] +Type=partition +Path=$blockdev +MatchPattern=part2-@v +MatchPartitionType=root-x86-64-verity +EOF + + cat >/var/tmp/72-defs/03-third.conf <<EOF +[Source] +Type=directory +Path=/var/tmp/72-source +MatchPattern=dir-@v + +[Target] +Type=directory +Path=/var/tmp/72-dirs +CurrentSymlink=/var/tmp/72-dirs/current +MatchPattern=dir-@v +InstancesMax=3 +EOF + + cat >/var/tmp/72-defs/04-fourth.conf <<EOF +[Source] +Type=regular-file +Path=/var/tmp/72-source +MatchPattern=uki-@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=uki_@v+@l-@d.efi \ + uki_@v+@l.efi \ + uki_@v.efi +Mode=0444 +TriesLeft=3 +TriesDone=0 +InstancesMax=2 +EOF + + cat >/var/tmp/72-defs/05-fifth.conf <<EOF +[Source] +Type=regular-file +Path=/var/tmp/72-source +MatchPattern=uki-extra-@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=uki_@v.efi.extra.d/extra.addon.efi +Mode=0444 +InstancesMax=2 +EOF + + rm -rf /var/tmp/72-esp /var/tmp/72-xbootldr + mkdir -p /var/tmp/72-esp/EFI/Linux /var/tmp/72-xbootldr/EFI/Linux + + rm -rf /var/tmp/72-source + mkdir -p /var/tmp/72-source + + # Install initial version and verify + new_version "$sector_size" v1 + update_now + verify_version "$blockdev" "$sector_size" v1 1 3 + + # Create second version, update and verify that it is added + new_version "$sector_size" v2 + update_now + verify_version "$blockdev" "$sector_size" v2 2 4 + + # Create third version, update and verify it replaced the first version + new_version "$sector_size" v3 + update_now + verify_version "$blockdev" "$sector_size" v3 1 3 + test ! -f "/var/tmp/72-xbootldr/EFI/Linux/uki_v1+3-0.efi" + test ! -f "/var/tmp/72-xbootldr/EFI/Linux/uki_v1.efi.extra.d/extra.addon.efi" + test ! -d "/var/tmp/72-xbootldr/EFI/Linux/uki_v1.efi.extra.d" + + # Create fourth version, and update through a file:// URL. This should be + # almost as good as testing HTTP, but is simpler for us to set up. file:// is + # abstracted in curl for us, and since our main goal is to test our own code + # (and not curl) this test should be quite good even if not comprehensive. This + # will test the SHA256SUMS logic at least (we turn off GPG validation though, + # see above) + new_version "$sector_size" v4 + + cat >/var/tmp/72-defs/02-second.conf <<EOF +[Source] +Type=url-file +Path=file:///var/tmp/72-source +MatchPattern=part2-@v.raw.gz + +[Target] +Type=partition +Path=$blockdev +MatchPattern=part2-@v +MatchPartitionType=root-x86-64-verity +EOF + + cat >/var/tmp/72-defs/03-third.conf <<EOF +[Source] +Type=url-tar +Path=file:///var/tmp/72-source +MatchPattern=dir-@v.tar.gz + +[Target] +Type=directory +Path=/var/tmp/72-dirs +CurrentSymlink=/var/tmp/72-dirs/current +MatchPattern=dir-@v +InstancesMax=3 +EOF + + update_now + verify_version "$blockdev" "$sector_size" v4 2 4 + + # Cleanup + [ -b "$blockdev" ] && losetup --detach "$blockdev" + rm "$BACKING_FILE" +done + +rm -r /var/tmp/72-{dirs,defs,source,xbootldr,esp} + +touch /testok diff --git a/test/units/testsuite-73.service b/test/units/testsuite-73.service new file mode 100644 index 0000000..3ebd24d --- /dev/null +++ b/test/units/testsuite-73.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-73-LOCALE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-73.sh b/test/units/testsuite-73.sh new file mode 100755 index 0000000..df5af4b --- /dev/null +++ b/test/units/testsuite-73.sh @@ -0,0 +1,693 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +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 + +enable_debug() { + mkdir -p /run/systemd/system/systemd-localed.service.d + cat >>/run/systemd/system/systemd-localed.service.d/override.conf <<EOF +[Service] +Environment=SYSTEMD_LOG_LEVEL=debug +EOF + + mkdir -p /run/systemd/system/systemd-vconsole-setup.service.d + cat >>/run/systemd/system/systemd-vconsole-setup.service.d/override.conf <<EOF +[Unit] +StartLimitIntervalSec=0 + +[Service] +Environment=SYSTEMD_LOG_LEVEL=debug +EOF + + systemctl daemon-reload +} + +restore_locale() { + if [[ -d /usr/lib/locale/xx_XX.UTF-8 ]]; then + rmdir /usr/lib/locale/xx_XX.UTF-8 + fi + + if [[ -f /tmp/locale.conf.bak ]]; then + mv /tmp/locale.conf.bak /etc/locale.conf + else + rm -f /etc/locale.conf + fi + + if [[ -f /tmp/default-locale.bak ]]; then + mv /tmp/default-locale.bak /etc/default/locale + else + rm -f /etc/default/locale + rmdir --ignore-fail-on-non-empty /etc/default + fi + + if [[ -f /tmp/locale.gen.bak ]]; then + mv /tmp/locale.gen.bak /etc/locale.gen + else + rm -f /etc/locale.gen + fi +} + +testcase_locale() { + local i output + + if [[ -f /etc/locale.conf ]]; then + cp /etc/locale.conf /tmp/locale.conf.bak + fi + + # Debian/Ubuntu specific file + if [[ -f /etc/default/locale ]]; then + cp /etc/default/locale /tmp/default-locale.bak + fi + + if [[ -f /etc/locale.gen ]]; then + cp /etc/locale.gen /tmp/locale.gen.bak + fi + + # remove locale.conf to make /etc/default/locale used by Debian/Ubuntu + rm -f /etc/locale.conf + # also remove /etc/default/locale + rm -f /etc/default/locale + # and create /etc/default to make /etc/default/locale created by localed + mkdir -p /etc/default + + trap restore_locale RETURN + + if command -v locale-gen >/dev/null 2>&1 && + ! localectl list-locales | grep -F "en_US.UTF-8"; then + # ensure at least one utf8 locale exist + echo "en_US.UTF-8 UTF-8" >/etc/locale.gen + locale-gen en_US.UTF-8 + fi + + # create invalid locale + mkdir -p /usr/lib/locale/xx_XX.UTF-8 + assert_not_in "xx_XX.UTF-8" "$(localectl list-locales)" + + if [[ -z "$(localectl list-locales)" ]]; then + echo "No locale installed, skipping test." + return + fi + + # start with a known default environment and make sure to also give a + # default value to LC_CTYPE= since we're about to also set/unset it. We + # also reload PID1 configuration to make sure that PID1 environment itself + # is updated as it's not always been the case. + assert_rc 0 localectl set-locale "LANG=en_US.UTF-8" "LC_CTYPE=C" + systemctl daemon-reload + output=$(localectl) + assert_in "System Locale: LANG=en_US.UTF-8" "$output" + assert_in "LC_CTYPE=C" "$output" + output=$(systemctl show-environment) + assert_in "LANG=en_US.UTF-8" "$output" + assert_in "LC_CTYPE=C" "$output" + + # warn when kernel command line has locale settings + output=$(SYSTEMD_PROC_CMDLINE="locale.LANG=C.UTF-8 locale.LC_CTYPE=ja_JP.UTF-8" localectl 2>&1) + assert_in "Warning:" "$output" + assert_in "Command Line: LANG=C.UTF-8" "$output" + assert_in "LC_CTYPE=ja_JP.UTF-8" "$output" + assert_in "System Locale:" "$output" + + # change locale + for i in $(localectl list-locales); do + assert_rc 0 localectl set-locale "LANG=C" "LC_CTYPE=$i" + if [[ -f /etc/default/locale ]]; then + assert_eq "$(cat /etc/default/locale)" "LANG=C +LC_CTYPE=$i" + else + assert_eq "$(cat /etc/locale.conf)" "LANG=C +LC_CTYPE=$i" + fi + output=$(localectl) + assert_in "System Locale: LANG=C" "$output" + assert_in "LC_CTYPE=$i" "$output" + output=$(systemctl show-environment) + assert_in "LANG=C" "$output" + assert_in "LC_CTYPE=$i" "$output" + + assert_rc 0 localectl set-locale "$i" + if [[ -f /etc/default/locale ]]; then + assert_eq "$(cat /etc/default/locale)" "LANG=$i" + else + assert_eq "$(cat /etc/locale.conf)" "LANG=$i" + fi + output=$(localectl) + assert_in "System Locale: LANG=$i" "$output" + assert_not_in "LC_CTYPE=" "$output" + output=$(systemctl show-environment) + assert_in "LANG=$i" "$output" + assert_not_in "LC_CTYPE=" "$output" + done + + # test if localed auto-runs locale-gen + if command -v locale-gen >/dev/null 2>&1 && + ! localectl list-locales | grep -F "de_DE.UTF-8"; then + + # clear previous locale + systemctl stop systemd-localed.service + rm -f /etc/locale.conf /etc/default/locale + + # change locale + assert_rc 0 localectl set-locale de_DE.UTF-8 + if [[ -f /etc/default/locale ]]; then + assert_eq "$(cat /etc/default/locale)" "LANG=de_DE.UTF-8" + else + assert_eq "$(cat /etc/locale.conf)" "LANG=de_DE.UTF-8" + fi + assert_in "System Locale: LANG=de_DE.UTF-8" "$(localectl)" + assert_in "LANG=de_DE.UTF-8" "$(systemctl show-environment)" + + # ensure tested locale exists and works now + assert_in "de_DE.UTF-8" "$(localectl list-locales)" + fi +} + +backup_keymap() { + if [[ -f /etc/vconsole.conf ]]; then + cp /etc/vconsole.conf /tmp/vconsole.conf.bak + fi + + if [[ -f /etc/X11/xorg.conf.d/00-keyboard.conf ]]; then + cp /etc/X11/xorg.conf.d/00-keyboard.conf /tmp/00-keyboard.conf.bak + fi + + # Debian/Ubuntu specific file + if [[ -f /etc/default/keyboard ]]; then + cp /etc/default/keyboard /tmp/default-keyboard.bak + fi + + mkdir -p /etc/default +} + +restore_keymap() { + if [[ -f /tmp/vconsole.conf.bak ]]; then + mv /tmp/vconsole.conf.bak /etc/vconsole.conf + else + rm -f /etc/vconsole.conf + fi + + if [[ -f /tmp/00-keyboard.conf.bak ]]; then + mv /tmp/00-keyboard.conf.bak /etc/X11/xorg.conf.d/00-keyboard.conf + else + rm -f /etc/X11/xorg.conf.d/00-keyboard.conf + fi + + if [[ -f /tmp/default-keyboard.bak ]]; then + mv /tmp/default-keyboard.bak /etc/default/keyboard + else + rm -f /etc/default/keyboard + rmdir --ignore-fail-on-non-empty /etc/default + fi +} + +wait_vconsole_setup() { + local i ss + for i in {1..20}; do + (( i > 1 )) && sleep 0.5 + ss="$(systemctl --property SubState --value show systemd-vconsole-setup.service)" + if [[ "$ss" == "exited" || "$ss" == "dead" || "$ss" == "condition" ]]; then + return 0 + elif [[ "$ss" == "failed" ]]; then + echo "WARNING: systemd-vconsole-setup.service failed, ignoring." >&2 + systemctl reset-failed systemd-vconsole-setup.service + return 0 + fi + done + + systemctl status systemd-vconsole-setup.service + return 1 +} + +testcase_vc_keymap() { + local i output vc + + if [[ -z "$(localectl list-keymaps)" ]]; then + echo "No vconsole keymap installed, skipping test." + return + fi + + backup_keymap + trap restore_keymap RETURN + + # should activate daemon and work + assert_in "VC Keymap:" "$(localectl)" + + for i in $(localectl list-keymaps); do + # clear previous conversion from VC -> X11 keymap + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/vconsole.conf /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + + # set VC keymap + assert_rc 0 localectl set-keymap "$i" + output=$(localectl) + + # check VC keymap + vc=$(cat /etc/vconsole.conf) + assert_in "KEYMAP=$i" "$vc" + assert_in "VC Keymap: $i" "$output" + + # check VC -> X11 keymap conversion + if [[ "$i" == "us" ]]; then + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: pc105\+inet" "$output" + assert_not_in "X11 Variant:" "$output" + assert_in "X11 Options: terminate:ctrl_alt_bksp" "$output" + + assert_in "XKBLAYOUT=us" "$vc" + assert_in "XKBMODEL=pc105\+inet" "$vc" + assert_not_in "XKBVARIANT" "$vc" + assert_in "XKBOPTIONS=terminate:ctrl_alt_bksp" "$vc" + elif [[ "$i" == "us-acentos" ]]; then + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: pc105" "$output" + assert_in "X11 Variant: intl" "$output" + assert_in "X11 Options: terminate:ctrl_alt_bksp" "$output" + + assert_in "XKBLAYOUT=us" "$vc" + assert_in "XKBMODEL=pc105" "$vc" + assert_in "XKBVARIANT=intl" "$vc" + assert_in "XKBOPTIONS=terminate:ctrl_alt_bksp" "$vc" + elif [[ "$i" =~ ^us-.* ]]; then + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: microsoftpro" "$output" + assert_in "X11 Variant:" "$output" + assert_in "X11 Options: terminate:ctrl_alt_bksp" "$output" + + assert_in "XKBLAYOUT=us" "$vc" + assert_in "XKBMODEL=microsoftpro" "$vc" + assert_in "XKBVARIANT=" "$vc" + assert_in "XKBOPTIONS=terminate:ctrl_alt_bksp" "$vc" + fi + done + + # gets along without config file + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/vconsole.conf + assert_in "VC Keymap: .unset." "$(localectl)" +} + +testcase_x11_keymap() { + local output + + if [[ -z "$(localectl list-x11-keymap-layouts)" ]]; then + echo "No x11 keymap installed, skipping test." + return + fi + + backup_keymap + trap restore_keymap RETURN + + # should activate daemon and work + assert_in "X11 Layout:" "$(localectl)" + + # set x11 keymap (layout, model, variant, options) + assert_rc 0 localectl set-x11-keymap us pc105+inet intl terminate:ctrl_alt_bksp + + if [[ -f /etc/default/keyboard ]]; then + assert_eq "$(cat /etc/default/keyboard)" "XKBLAYOUT=us +XKBMODEL=pc105+inet +XKBVARIANT=intl +XKBOPTIONS=terminate:ctrl_alt_bksp" + else + output=$(cat /etc/X11/xorg.conf.d/00-keyboard.conf) + assert_in 'Option "XkbLayout" "us"' "$output" + assert_in 'Option "XkbModel" "pc105\+inet"' "$output" + assert_in 'Option "XkbVariant" "intl"' "$output" + assert_in 'Option "XkbOptions" "terminate:ctrl_alt_bksp"' "$output" + + output=$(cat /etc/vconsole.conf) + assert_in 'XKBLAYOUT=us' "$output" + assert_in 'XKBMODEL=pc105\+inet' "$output" + assert_in 'XKBVARIANT=intl' "$output" + assert_in 'XKBOPTIONS=terminate:ctrl_alt_bksp' "$output" + fi + + output=$(localectl) + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: pc105\+inet" "$output" + assert_in "X11 Variant: intl" "$output" + assert_in "X11 Options: terminate:ctrl_alt_bksp" "$output" + + # Debian/Ubuntu patch is buggy, unspecified settings are not cleared + rm -f /etc/default/keyboard + + # set x11 keymap (layout, model, variant) + assert_rc 0 localectl set-x11-keymap us pc105+inet intl + + if [[ -f /etc/default/keyboard ]]; then + assert_eq "$(cat /etc/default/keyboard)" "XKBLAYOUT=us +XKBMODEL=pc105+inet +XKBVARIANT=intl" + else + output=$(cat /etc/X11/xorg.conf.d/00-keyboard.conf) + assert_in 'Option "XkbLayout" "us"' "$output" + assert_in 'Option "XkbModel" "pc105\+inet"' "$output" + assert_in 'Option "XkbVariant" "intl"' "$output" + assert_not_in 'Option "XkbOptions"' "$output" + + output=$(cat /etc/vconsole.conf) + assert_in 'XKBLAYOUT=us' "$output" + assert_in 'XKBMODEL=pc105\+inet' "$output" + assert_in 'XKBVARIANT=intl' "$output" + assert_not_in 'XKBOPTIONS' "$output" + fi + + output=$(localectl) + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: pc105\+inet" "$output" + assert_in "X11 Variant: intl" "$output" + assert_not_in "X11 Options:" "$output" + + # Debian/Ubuntu patch is buggy, unspecified settings are not cleared + rm -f /etc/default/keyboard + + # set x11 keymap (layout, model) + assert_rc 0 localectl set-x11-keymap us pc105+inet + + if [[ -f /etc/default/keyboard ]]; then + assert_eq "$(cat /etc/default/keyboard)" "XKBLAYOUT=us +XKBMODEL=pc105+inet" + else + output=$(cat /etc/X11/xorg.conf.d/00-keyboard.conf) + assert_in 'Option "XkbLayout" "us"' "$output" + assert_in 'Option "XkbModel" "pc105\+inet"' "$output" + assert_not_in 'Option "XkbVariant"' "$output" + assert_not_in 'Option "XkbOptions"' "$output" + + output=$(cat /etc/vconsole.conf) + assert_in 'XKBLAYOUT=us' "$output" + assert_in 'XKBMODEL=pc105\+inet' "$output" + assert_not_in 'XKBVARIANT' "$output" + assert_not_in 'XKBOPTIONS' "$output" + fi + + output=$(localectl) + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: pc105\+inet" "$output" + assert_not_in "X11 Variant:" "$output" + assert_not_in "X11 Options:" "$output" + + # Debian/Ubuntu patch is buggy, unspecified settings are not cleared + rm -f /etc/default/keyboard + + # set x11 keymap (layout) + assert_rc 0 localectl set-x11-keymap us + + if [[ -f /etc/default/keyboard ]]; then + assert_eq "$(cat /etc/default/keyboard)" "XKBLAYOUT=us" + else + output=$(cat /etc/X11/xorg.conf.d/00-keyboard.conf) + assert_in 'Option "XkbLayout" "us"' "$output" + assert_not_in 'Option "XkbModel"' "$output" + assert_not_in 'Option "XkbVariant"' "$output" + assert_not_in 'Option "XkbOptions"' "$output" + + output=$(cat /etc/vconsole.conf) + assert_in 'XKBLAYOUT=us' "$output" + assert_not_in 'XKBMODEL' "$output" + assert_not_in 'XKBVARIANT' "$output" + assert_not_in 'XKBOPTIONS' "$output" + fi + + output=$(localectl) + assert_in "X11 Layout: us" "$output" + assert_not_in "X11 Model:" "$output" + assert_not_in "X11 Variant:" "$output" + assert_not_in "X11 Options:" "$output" + + # gets along without config file + systemctl stop systemd-localed.service + rm -f /etc/vconsole.conf /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + output=$(localectl) + assert_in "X11 Layout: .unset." "$output" + assert_not_in "X11 Model:" "$output" + assert_not_in "X11 Variant:" "$output" + assert_not_in "X11 Options:" "$output" +} + +testcase_convert() { + if [[ -z "$(localectl list-keymaps)" ]]; then + echo "No vconsole keymap installed, skipping test." + return + fi + + if [[ -z "$(localectl list-x11-keymap-layouts)" ]]; then + echo "No x11 keymap installed, skipping test." + return + fi + + backup_keymap + trap restore_keymap RETURN + + # clear previous settings + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/vconsole.conf /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + + # set VC keymap without conversion + assert_rc 0 localectl --no-convert set-keymap us + output=$(localectl) + + # check VC keymap + vc=$(cat /etc/vconsole.conf) + assert_in "KEYMAP=us" "$vc" + assert_in "VC Keymap: us" "$output" + + # check VC -> X11 keymap conversion (nothing set) + assert_in "X11 Layout: .unset." "$output" + assert_not_in "X11 Model:" "$output" + assert_not_in "X11 Variant:" "$output" + assert_not_in "X11 Options:" "$output" + + assert_not_in "XKBLAYOUT=" "$vc" + assert_not_in "XKBMODEL=" "$vc" + assert_not_in "XKBVARIANT=" "$vc" + assert_not_in "XKBOPTIONS=" "$vc" + + # set VC keymap with conversion + assert_rc 0 localectl set-keymap us + output=$(localectl) + + # check VC keymap + vc=$(cat /etc/vconsole.conf) + assert_in "KEYMAP=us" "$vc" + assert_in "VC Keymap: us" "$output" + + # check VC -> X11 keymap conversion + assert_in "X11 Layout: us" "$output" + assert_in "X11 Model: pc105\+inet" "$output" + assert_not_in "X11 Variant:" "$output" + assert_in "X11 Options: terminate:ctrl_alt_bksp" "$output" + + assert_in "XKBLAYOUT=us" "$vc" + assert_in "XKBMODEL=pc105\+inet" "$vc" + assert_not_in "XKBVARIANT" "$vc" + assert_in "XKBOPTIONS=terminate:ctrl_alt_bksp" "$vc" + + # clear previous settings + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/vconsole.conf /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + + # set x11 keymap (layout) without conversion + assert_rc 0 localectl --no-convert set-x11-keymap us + + assert_not_in "KEYMAP=" "$(cat /etc/vconsole.conf)" + assert_in "VC Keymap: .unset." "$(localectl)" + + if [[ -f /etc/default/keyboard ]]; then + assert_eq "$(cat /etc/default/keyboard)" "XKBLAYOUT=us" + else + output=$(cat /etc/X11/xorg.conf.d/00-keyboard.conf) + assert_in 'Option "XkbLayout" "us"' "$output" + assert_not_in 'Option "XkbModel"' "$output" + assert_not_in 'Option "XkbVariant"' "$output" + assert_not_in 'Option "XkbOptions"' "$output" + + output=$(cat /etc/vconsole.conf) + assert_in 'XKBLAYOUT=us' "$output" + assert_not_in 'XKBMODEL=' "$output" + assert_not_in 'XKBVARIANT=' "$output" + assert_not_in 'XKBOPTIONS=' "$output" + fi + + output=$(localectl) + assert_in "X11 Layout: us" "$output" + assert_not_in "X11 Model:" "$output" + assert_not_in "X11 Variant:" "$output" + assert_not_in "X11 Options:" "$output" + + # set x11 keymap (layout, model) with conversion + assert_rc 0 localectl set-x11-keymap us + + assert_in "KEYMAP=us" "$(cat /etc/vconsole.conf)" + assert_in "VC Keymap: us" "$(localectl)" + + if [[ -f /etc/default/keyboard ]]; then + assert_eq "$(cat /etc/default/keyboard)" "XKBLAYOUT=us" + else + output=$(cat /etc/X11/xorg.conf.d/00-keyboard.conf) + assert_in 'Option "XkbLayout" "us"' "$output" + assert_not_in 'Option "XkbModel"' "$output" + assert_not_in 'Option "XkbVariant"' "$output" + assert_not_in 'Option "XkbOptions"' "$output" + + output=$(cat /etc/vconsole.conf) + assert_in 'XKBLAYOUT=us' "$output" + assert_not_in 'XKBMODEL=' "$output" + assert_not_in 'XKBVARIANT=' "$output" + assert_not_in 'XKBOPTIONS=' "$output" + fi + + output=$(localectl) + assert_in "X11 Layout: us" "$output" + assert_not_in "X11 Model:" "$output" + assert_not_in "X11 Variant:" "$output" + assert_not_in "X11 Options:" "$output" +} + +testcase_validate() { + if [[ -z "$(localectl list-keymaps)" ]]; then + echo "No vconsole keymap installed, skipping test." + return + fi + + if [[ -z "$(localectl list-x11-keymap-layouts)" ]]; then + echo "No x11 keymap installed, skipping test." + return + fi + + backup_keymap + trap restore_keymap RETURN + + # clear previous settings + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + + # create invalid configs + cat >/etc/vconsole.conf <<EOF +KEYMAP=foobar +XKBLAYOUT=hogehoge +EOF + + # confirm that the invalid settings are not shown + output=$(localectl) + assert_in "VC Keymap: .unset." "$output" + if [[ "$output" =~ "X11 Layout: hogehoge" ]]; then + # Debian/Ubuntu build systemd without xkbcommon. + echo "systemd built without xkbcommon, skipping test." + return + fi + assert_in "X11 Layout: .unset." "$output" + + # only update the virtual console keymap + assert_rc 0 localectl --no-convert set-keymap us + + output=$(localectl) + assert_in "VC Keymap: us" "$output" + assert_in "X11 Layout: .unset." "$output" + + output=$(cat /etc/vconsole.conf) + assert_in "KEYMAP=us" "$output" + assert_not_in "XKBLAYOUT=" "$output" + + # clear previous settings + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + + # create invalid configs + cat >/etc/vconsole.conf <<EOF +KEYMAP=foobar +XKBLAYOUT=hogehoge +EOF + + # confirm that the invalid settings are not shown + output=$(localectl) + assert_in "VC Keymap: .unset." "$output" + assert_in "X11 Layout: .unset." "$output" + + # only update the X11 keyboard layout + assert_rc 0 localectl --no-convert set-x11-keymap us + + output=$(localectl) + assert_in "VC Keymap: .unset." "$output" + assert_in "X11 Layout: us" "$output" + + output=$(cat /etc/vconsole.conf) + assert_not_in "KEYMAP=" "$output" + assert_in "XKBLAYOUT=us" "$output" + + # clear previous settings + systemctl stop systemd-localed.service + wait_vconsole_setup + rm -f /etc/X11/xorg.conf.d/00-keyboard.conf /etc/default/keyboard + + # create invalid configs + cat >/etc/vconsole.conf <<EOF +KEYMAP=foobar +XKBLAYOUT=hogehoge +EOF + + # update the virtual console keymap with conversion + assert_rc 0 localectl set-keymap us + + output=$(localectl) + assert_in "VC Keymap: us" "$output" + assert_in "X11 Layout: us" "$output" + + output=$(cat /etc/vconsole.conf) + assert_in "KEYMAP=us" "$output" + assert_in "XKBLAYOUT=us" "$output" +} + +locale_gen_cleanup() { + # Some running apps might keep the mount point busy, hence the lazy unmount + mountpoint -q /usr/lib/locale && umount --lazy /usr/lib/locale + [[ -e /tmp/locale.gen.bak ]] && mv -f /tmp/locale.gen.bak /etc/locale.gen + + return 0 +} + +# Issue: https://github.com/systemd/systemd/pull/27179 +testcase_locale_gen_leading_space() { + if ! command -v locale-gen >/dev/null; then + echo "No locale-gen support, skipping test." + return 0 + fi + + [[ -e /etc/locale.gen ]] && cp -f /etc/locale.gen /tmp/locale.gen.bak + trap locale_gen_cleanup RETURN + # Overmount the existing locale-gen database with an empty directory + # to force it to regenerate locales + mount -t tmpfs tmpfs /usr/lib/locale + + { + echo -e "en_US.UTF-8 UTF-8" + echo -e " en_US.UTF-8 UTF-8" + echo -e "\ten_US.UTF-8 UTF-8" + echo -e " \t en_US.UTF-8 UTF-8 \t" + } >/etc/locale.gen + + localectl set-locale de_DE.UTF-8 + localectl set-locale en_US.UTF-8 +} + +# Make sure the content of kbd-model-map is the one that the tests expect +# regardless of the version installed on the distro where the testsuite is +# running on. +export SYSTEMD_KBD_MODEL_MAP=/usr/lib/systemd/tests/testdata/test-keymap-util/kbd-model-map + +enable_debug +run_testcases + +touch /testok diff --git a/test/units/testsuite-74.battery-check.sh b/test/units/testsuite-74.battery-check.sh new file mode 100755 index 0000000..52a92b8 --- /dev/null +++ b/test/units/testsuite-74.battery-check.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +/usr/lib/systemd/systemd-battery-check --help +/usr/lib/systemd/systemd-battery-check --version + +/usr/lib/systemd/systemd-battery-check || : diff --git a/test/units/testsuite-74.bootctl.sh b/test/units/testsuite-74.bootctl.sh new file mode 100755 index 0000000..4be7bfd --- /dev/null +++ b/test/units/testsuite-74.bootctl.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if systemd-detect-virt --quiet --container; then + echo "running on container, skipping." + exit 0 +fi + +if ! command -v bootctl >/dev/null; then + echo "bootctl not found, skipping." + exit 0 +fi + +if [[ ! -d /usr/lib/systemd/boot/efi ]]; then + echo "sd-boot is not installed, skipping." + exit 0 +fi + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +basic_tests() { + bootctl "$@" --help + bootctl "$@" --version + + bootctl "$@" install --make-entry-directory=yes + bootctl "$@" remove --make-entry-directory=yes + + bootctl "$@" install --all-architectures + bootctl "$@" remove --all-architectures + + bootctl "$@" install --make-entry-directory=yes --all-architectures + bootctl "$@" remove --make-entry-directory=yes --all-architectures + + bootctl "$@" install + (! bootctl "$@" update) + bootctl "$@" update --graceful + + bootctl "$@" is-installed + bootctl "$@" is-installed --graceful + bootctl "$@" random-seed + + bootctl "$@" + bootctl "$@" status + bootctl "$@" status --quiet + bootctl "$@" list + bootctl "$@" list --quiet + bootctl "$@" list --json=short + bootctl "$@" list --json=pretty + + bootctl "$@" remove + (! bootctl "$@" is-installed) + (! bootctl "$@" is-installed --graceful) +} + +testcase_bootctl_basic() { + assert_eq "$(bootctl --print-esp-path)" "/efi" + assert_eq "$(bootctl --print-boot-path)" "/boot" + bootctl --print-root-device + + basic_tests +} + +cleanup_image() ( + set +e + + if [[ -z "${IMAGE_DIR:-}" ]]; then + return 0 + fi + + umount "${IMAGE_DIR}/root" + + if [[ -n "${LOOPDEV:-}" ]]; then + losetup -d "${LOOPDEV}" + unset LOOPDEV + fi + + udevadm settle + + rm -rf "${IMAGE_DIR}" + unset IMAGE_DIR + + return 0 +) + +testcase_bootctl_image() { + IMAGE_DIR="$(mktemp --directory /tmp/test-bootctl.XXXXXXXXXX)" + trap cleanup_image RETURN + + truncate -s 256m "${IMAGE_DIR}/image" + + cat >"${IMAGE_DIR}/partscript" <<EOF +label: gpt +type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B name=esp size=64M +type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=root size=64M bootable +type=BC13C2FF-59E6-4262-A352-B275FD6F7172 name=boot +EOF + + LOOPDEV="$(losetup --show -P -f "${IMAGE_DIR}/image")" + sfdisk "$LOOPDEV" <"${IMAGE_DIR}/partscript" + + udevadm settle + + mkfs.vfat -n esp "${LOOPDEV}p1" + mkfs.ext4 -L root "${LOOPDEV}p2" + mkfs.ext4 -L boot "${LOOPDEV}p3" + + mkdir -p "${IMAGE_DIR}/root" + mount -t ext4 "${LOOPDEV}p2" "${IMAGE_DIR}/root" + + mkdir -p "${IMAGE_DIR}/root/efi" + mkdir -p "${IMAGE_DIR}/root/boot" + mkdir -p "${IMAGE_DIR}/root/etc" + mkdir -p "${IMAGE_DIR}/root/usr/lib" + if [[ -f /usr/lib/os-release ]]; then + cp /usr/lib/os-release "${IMAGE_DIR}/root/usr/lib/." + ln -s ../usr/lib/os-release "${IMAGE_DIR}/root/etc/os-release" + else + cp -a /etc/os-release "${IMAGE_DIR}/root/etc/." + fi + + umount "${IMAGE_DIR}/root" + + assert_eq "$(bootctl --image "${IMAGE_DIR}/image" --print-esp-path)" "/run/systemd/mount-rootfs/efi" + assert_eq "$(bootctl --image "${IMAGE_DIR}/image" --print-esp-path --esp-path=/efi)" "/run/systemd/mount-rootfs/efi" + assert_eq "$(bootctl --image "${IMAGE_DIR}/image" --print-boot-path)" "/run/systemd/mount-rootfs/boot" + assert_eq "$(bootctl --image "${IMAGE_DIR}/image" --print-boot-path --boot-path=/boot)" "/run/systemd/mount-rootfs/boot" + + # FIXME: This provides spurious result. + bootctl --image "${IMAGE_DIR}/image" --print-root-device || : + + basic_tests --image "${IMAGE_DIR}/image" +} + +cleanup_raid() ( + set +e + + if [[ -z "${IMAGE_DIR:-}" ]]; then + return 0 + fi + + systemd-umount "${IMAGE_DIR}/root/efi" + systemd-umount "${IMAGE_DIR}/root/boot" + systemd-umount "${IMAGE_DIR}/root" + + mdadm --misc --stop /dev/md/raid-esp + mdadm --misc --stop /dev/md/raid-root + + if [[ -n "${LOOPDEV1:-}" ]]; then + mdadm --misc --force --zero-superblock "${LOOPDEV1}p1" + mdadm --misc --force --zero-superblock "${LOOPDEV1}p2" + fi + + if [[ -n "${LOOPDEV2:-}" ]]; then + mdadm --misc --force --zero-superblock "${LOOPDEV2}p1" + mdadm --misc --force --zero-superblock "${LOOPDEV2}p2" + fi + + udevadm settle + + if [[ -n "${LOOPDEV1:-}" ]]; then + mdadm --misc --force --zero-superblock "${LOOPDEV1}p1" + mdadm --misc --force --zero-superblock "${LOOPDEV1}p2" + losetup -d "${LOOPDEV1}" + unset LOOPDEV1 + fi + + if [[ -n "${LOOPDEV2:-}" ]]; then + mdadm --misc --force --zero-superblock "${LOOPDEV2}p1" + mdadm --misc --force --zero-superblock "${LOOPDEV2}p2" + losetup -d "${LOOPDEV2}" + unset LOOPDEV2 + fi + + udevadm settle + + rm -rf "${IMAGE_DIR}" + + return 0 +) + +testcase_bootctl_raid() { + if ! command -v mdadm >/dev/null; then + echo "mdadm not found, skipping." + return 0 + fi + + if ! command -v mkfs.btrfs >/dev/null; then + echo "mkfs.btrfs not found, skipping." + return 0 + fi + + IMAGE_DIR="$(mktemp --directory /tmp/test-bootctl.XXXXXXXXXX)" + trap cleanup_raid RETURN + + truncate -s 256m "${IMAGE_DIR}/image1" + truncate -s 256m "${IMAGE_DIR}/image2" + + cat >"${IMAGE_DIR}/partscript" <<EOF +label: gpt +type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B name=esp size=64M +type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=root size=64M bootable +type=BC13C2FF-59E6-4262-A352-B275FD6F7172 name=boot +EOF + + LOOPDEV1="$(losetup --show -P -f "${IMAGE_DIR}/image1")" + LOOPDEV2="$(losetup --show -P -f "${IMAGE_DIR}/image2")" + sfdisk "$LOOPDEV1" <"${IMAGE_DIR}/partscript" + sfdisk "$LOOPDEV2" <"${IMAGE_DIR}/partscript" + + udevadm settle + + echo y | mdadm --create /dev/md/raid-esp --name "raid-esp" "${LOOPDEV1}p1" "${LOOPDEV2}p1" -v -f --level=1 --raid-devices=2 + mkfs.vfat /dev/md/raid-esp + echo y | mdadm --create /dev/md/raid-root --name "raid-root" "${LOOPDEV1}p2" "${LOOPDEV2}p2" -v -f --level=1 --raid-devices=2 + mkfs.ext4 /dev/md/raid-root + mkfs.btrfs -f -M -d raid1 -m raid1 -L "raid-boot" "${LOOPDEV1}p3" "${LOOPDEV2}p3" + + mkdir -p "${IMAGE_DIR}/root" + mount -t ext4 /dev/md/raid-root "${IMAGE_DIR}/root" + mkdir -p "${IMAGE_DIR}/root/efi" + mount -t vfat /dev/md/raid-esp "${IMAGE_DIR}/root/efi" + mkdir -p "${IMAGE_DIR}/root/boot" + mount -t btrfs "${LOOPDEV1}p3" "${IMAGE_DIR}/root/boot" + + mkdir -p "${IMAGE_DIR}/root/etc" + mkdir -p "${IMAGE_DIR}/root/usr/lib" + if [[ -f /usr/lib/os-release ]]; then + cp /usr/lib/os-release "${IMAGE_DIR}/root/usr/lib/." + ln -s ../usr/lib/os-release "${IMAGE_DIR}/root/etc/os-release" + else + cp -a /etc/os-release "${IMAGE_DIR}/root/etc/." + fi + + # find_esp() does not support md RAID partition. + (! bootctl --root "${IMAGE_DIR}/root" --print-esp-path) + (! bootctl --root "${IMAGE_DIR}/root" --print-esp-path --esp-path=/efi) + + # If the verification is relaxed, it accepts md RAID partition. + assert_eq "$(SYSTEMD_RELAX_ESP_CHECKS=yes bootctl --root "${IMAGE_DIR}/root" --print-esp-path)" "${IMAGE_DIR}/root/efi" + assert_eq "$(SYSTEMD_RELAX_ESP_CHECKS=yes bootctl --root "${IMAGE_DIR}/root" --print-esp-path --esp-path=/efi)" "${IMAGE_DIR}/root/efi" + + # find_xbootldr() does not support btrfs RAID, and bootctl tries to fall back to use ESP. + # (but as in the above, the ESP verification is also failed in this case). + (! bootctl --root "${IMAGE_DIR}/root" --print-boot-path) + (! bootctl --root "${IMAGE_DIR}/root" --print-boot-path --boot-path=/boot) + + # If the verification for ESP is relaxed, bootctl falls back to use ESP. + assert_eq "$(SYSTEMD_RELAX_ESP_CHECKS=yes bootctl --root "${IMAGE_DIR}/root" --print-boot-path)" "${IMAGE_DIR}/root/efi" + + # If the verification is relaxed, it accepts the xbootldr partition. + assert_eq "$(SYSTEMD_RELAX_XBOOTLDR_CHECKS=yes bootctl --root "${IMAGE_DIR}/root" --print-boot-path)" "${IMAGE_DIR}/root/boot" + assert_eq "$(SYSTEMD_RELAX_XBOOTLDR_CHECKS=yes bootctl --root "${IMAGE_DIR}/root" --print-boot-path --boot-path=/boot)" "${IMAGE_DIR}/root/boot" + + # FIXME: This provides spurious result. + bootctl --root "${IMAGE_DIR}/root" --print-root-device || : + + SYSTEMD_RELAX_ESP_CHECKS=yes SYSTEMD_RELAX_XBOOTLDR_CHECKS=yes basic_tests --root "${IMAGE_DIR}/root" +} + +run_testcases diff --git a/test/units/testsuite-74.busctl.sh b/test/units/testsuite-74.busctl.sh new file mode 100755 index 0000000..aaf96d0 --- /dev/null +++ b/test/units/testsuite-74.busctl.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Unset $PAGER so we don't have to use --no-pager everywhere +export PAGER= + +busctl --help +busctl help +busctl --version +busctl +busctl list --no-pager --allow-interactive-authorization=no +busctl list +busctl list --unique --show-machine --full +# Pass the JSON output (-j) through jq to check if it's valid +busctl list --acquired --activatable --no-legend -j | jq +busctl status +busctl status --machine=.host --augment-creds=no +busctl status --user --machine=testuser@.host +busctl status org.freedesktop.systemd1 +# Ignore the exit code here, since this runs during machine bootup, so busctl +# might attempt to introspect a job that already finished and fail, i.e.: +# Failed to introspect object /org/freedesktop/systemd1/job/335 of service org.freedesktop.systemd1: Unknown object '/org/freedesktop/systemd1/job/335'. +busctl tree || : +busctl tree org.freedesktop.login1 +busctl tree --list org.freedesktop.login1 +busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1 +busctl introspect --watch-bind=yes --xml-interface org.freedesktop.systemd1 /org/freedesktop/LogControl1 +busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager + +busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + GetDefaultTarget +# Pass both JSON outputs through jq to check if the response JSON is valid +busctl call --json=pretty \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + ListUnitsByNames as 2 "systemd-journald.service" "systemd-logind.service" | jq +busctl call --json=short \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + ListUnitsByNames as 2 "systemd-journald.service" "systemd-logind.service" | jq +# Get all properties on the org.freedesktop.systemd1.Manager interface and dump +# them as JSON to exercise the internal JSON transformations +busctl call -j \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.DBus.Properties \ + GetAll s "org.freedesktop.systemd1.Manager" | jq -c +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" + +busctl emit /org/freedesktop/login1 org.freedesktop.login1.Manager \ + PrepareForSleep b false +busctl emit --auto-start=no --destination=systemd-logind.service \ + /org/freedesktop/login1 org.freedesktop.login1.Manager \ + PrepareForShutdown b false + +busctl get-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + Version +busctl get-property --verbose \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + LogLevel LogTarget SystemState Version +# Pass both JSON outputs through jq to check if the response JSON is valid +busctl get-property --json=pretty \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + LogLevel LogTarget SystemState Version | jq +busctl get-property --json=short \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + LogLevel LogTarget SystemState Version | jq + +# Set a property and check if it was indeed set +busctl set-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + KExecWatchdogUSec t 666 +busctl get-property -j \ + org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + KExecWatchdogUSec | jq -e '.data == 666' + +(! busctl status org.freedesktop.systemd2) +(! busctl tree org.freedesktop.systemd2) +(! busctl introspect org.freedesktop.systemd1) +(! busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd2) +(! busctl introspect org.freedesktop.systemd2 /org/freedesktop/systemd1) + +# Invalid method +(! busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + ThisMethodDoesntExist) +# Invalid signature +(! busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + ListUnitsByNames ab 1 false) +# Invalid arguments +(! busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + GetUnitByPID u "hello") +(! busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + -- ListUnitsByNames as -1 "systemd-journald.service") +# Not enough arguments +(! busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + ListUnitsByNames as 99 "systemd-journald.service") + +(! busctl get-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + NonexistentProperty) +(! busctl get-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + Version NonexistentProperty Version) + +# Invalid property +(! busctl set-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + NonexistentProperty t 666) +# Invalid signature +(! busctl set-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + KExecWatchdogUSec s 666) +# Invalid argument +(! busctl set-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager \ + KExecWatchdogUSec t "foo") diff --git a/test/units/testsuite-74.cgls.sh b/test/units/testsuite-74.cgls.sh new file mode 100755 index 0000000..9268f42 --- /dev/null +++ b/test/units/testsuite-74.cgls.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-cgls +systemd-cgls --all --full +systemd-cgls -k +systemd-cgls --xattr=yes +systemd-cgls --xattr=no +systemd-cgls --cgroup-id=yes +systemd-cgls --cgroup-id=no + +systemd-cgls /system.slice/systemd-journald.service +systemd-cgls /system.slice/systemd-journald.service /init.scope +systemd-cgls /sys/fs/cgroup/system.slice/systemd-journald.service /init.scope +[[ -d /sys/fs/cgroup/init.scope ]] && init_scope="init.scope" || init_scope="systemd/init.scope" +(cd "/sys/fs/cgroup/$init_scope" && systemd-cgls) +systemd-cgls --unit=systemd-journald.service +# There's most likely no user session running, so we need to create one +systemd-run --user --wait --pipe -M testuser@.host systemd-cgls --user-unit=app.slice + +(! systemd-cgls /foo/bar) +(! systemd-cgls --unit=hello.world) +(! systemd-cgls --user-unit=hello.world) +(! systemd-cgls --xattr=foo) +(! systemd-cgls --cgroup-id=foo) diff --git a/test/units/testsuite-74.cgtop.sh b/test/units/testsuite-74.cgtop.sh new file mode 100755 index 0000000..cf98279 --- /dev/null +++ b/test/units/testsuite-74.cgtop.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Without tty attached cgtop should default to --iterations=1 +systemd-cgtop +systemd-cgtop --iterations=1 +# Same as --iterations=1 +systemd-cgtop -1 +systemd-cgtop --delay=1ms +systemd-cgtop --raw +systemd-cgtop --batch +systemd-cgtop --cpu=percentage +systemd-cgtop --cpu=time +systemd-cgtop -P +systemd-cgtop -k +systemd-cgtop --recursive=no -P +systemd-cgtop --recursive=no -k +systemd-cgtop --depth=0 +systemd-cgtop --depth=100 + +for order in path tasks cpu memory io; do + systemd-cgtop --order="$order" +done +systemd-cgtop -p -t -c -m -i + +(! systemd-cgtop --cpu=foo) +(! systemd-cgtop --order=foo) +(! systemd-cgtop --depth=-1) +(! systemd-cgtop --recursive=foo) +(! systemd-cgtop --delay=1foo) diff --git a/test/units/testsuite-74.coredump.sh b/test/units/testsuite-74.coredump.sh new file mode 100755 index 0000000..6552643 --- /dev/null +++ b/test/units/testsuite-74.coredump.sh @@ -0,0 +1,221 @@ +#!/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 + +# Make sure the binary name fits into 15 characters +CORE_TEST_BIN="/tmp/test-dump" +CORE_TEST_UNPRIV_BIN="/tmp/test-usr-dump" +MAKE_DUMP_SCRIPT="/tmp/make-dump" +# Unset $PAGER so we don't have to use --no-pager everywhere +export PAGER= + +at_exit() { + rm -fv -- "$CORE_TEST_BIN" "$CORE_TEST_UNPRIV_BIN" "$MAKE_DUMP_SCRIPT" +} + +trap at_exit EXIT + +if systemd-detect-virt -cq; then + echo "Running in a container, skipping the systemd-coredump test..." + exit 0 +fi + +# To make all coredump entries stored in system.journal. +journalctl --rotate + +# Check that we're the ones to receive coredumps +sysctl kernel.core_pattern | grep systemd-coredump + +# Prepare "fake" binaries for coredumps, so we can properly exercise +# the matching stuff too +cp -vf /bin/sleep "${CORE_TEST_BIN:?}" +cp -vf /bin/sleep "${CORE_TEST_UNPRIV_BIN:?}" +# Simple script that spawns given "fake" binary and then kills it with +# given signal +cat >"${MAKE_DUMP_SCRIPT:?}" <<\EOF +#!/bin/bash -ex + +bin="${1:?}" +sig="${2:?}" + +ulimit -c unlimited +"$bin" infinity & +pid=$! +# Sync with the "fake" binary, so we kill it once it's fully forked off, +# otherwise we might kill it during fork and kernel would then report +# "wrong" binary name (i.e. $MAKE_DUMP_SCRIPT instead of $CORE_TEST_BIN). +# In this case, wait until the "fake" binary (sleep in this case) enters +# the "interruptible sleep" state, at which point it should be ready +# to be sacrificed. +for _ in {0..9}; do + read -ra self_stat <"/proc/$pid/stat" + [[ "${self_stat[2]}" == S ]] && break + sleep .5 +done +kill -s "$sig" "$pid" +# This should always fail +! wait "$pid" +EOF +chmod +x "$MAKE_DUMP_SCRIPT" + +# Privileged stuff +[[ "$(id -u)" -eq 0 ]] +# Trigger a couple of coredumps +"$MAKE_DUMP_SCRIPT" "$CORE_TEST_BIN" "SIGTRAP" +"$MAKE_DUMP_SCRIPT" "$CORE_TEST_BIN" "SIGABRT" +# In the tests we store the coredumps in journals, so let's generate a couple +# with Storage=external as well +mkdir -p /run/systemd/coredump.conf.d/ +printf '[Coredump]\nStorage=external' >/run/systemd/coredump.conf.d/99-external.conf +"$MAKE_DUMP_SCRIPT" "$CORE_TEST_BIN" "SIGTRAP" +"$MAKE_DUMP_SCRIPT" "$CORE_TEST_BIN" "SIGABRT" +rm -fv /run/systemd/coredump.conf.d/99-external.conf +# Wait a bit for the coredumps to get processed +timeout 30 bash -c "while [[ \$(coredumpctl list -q --no-legend $CORE_TEST_BIN | wc -l) -lt 4 ]]; do sleep 1; done" + +# Make sure we can forward crashes back to containers +CONTAINER="testsuite-74-container" + +mkdir -p "/var/lib/machines/$CONTAINER" +mkdir -p "/run/systemd/system/systemd-nspawn@$CONTAINER.service.d" +# Bind-mounting /etc into the container kinda defeats the purpose of --volatile=, +# but we need the ASan-related overrides scattered across /etc +cat > "/run/systemd/system/systemd-nspawn@$CONTAINER.service.d/override.conf" << EOF +[Service] +ExecStart= +ExecStart=systemd-nspawn --quiet --link-journal=try-guest --keep-unit --machine=%i --boot \ + --volatile=yes --directory=/ --bind-ro=/etc --inaccessible=/etc/machine-id +EOF +systemctl daemon-reload + +if cgroupfs_supports_user_xattrs; then + machinectl start "$CONTAINER" + timeout 60 bash -xec "until systemd-run -M '$CONTAINER' -q --wait --pipe true; do sleep .5; done" + + [[ "$(systemd-run -M "$CONTAINER" -q --wait --pipe coredumpctl list -q --no-legend /usr/bin/sleep | wc -l)" -eq 0 ]] + machinectl copy-to "$CONTAINER" "$MAKE_DUMP_SCRIPT" + systemd-run -M "$CONTAINER" -q --wait --pipe "$MAKE_DUMP_SCRIPT" "/usr/bin/sleep" "SIGABRT" + systemd-run -M "$CONTAINER" -q --wait --pipe "$MAKE_DUMP_SCRIPT" "/usr/bin/sleep" "SIGTRAP" + # Wait a bit for the coredumps to get processed + timeout 30 bash -c "while [[ \$(systemd-run -M $CONTAINER -q --wait --pipe coredumpctl list -q --no-legend /usr/bin/sleep | wc -l) -lt 2 ]]; do sleep 1; done" +fi + +coredumpctl +SYSTEMD_LOG_LEVEL=debug coredumpctl +coredumpctl --help +coredumpctl --version +coredumpctl --no-pager --no-legend +coredumpctl --all +coredumpctl -1 +coredumpctl -n 1 +coredumpctl --reverse +coredumpctl -F COREDUMP_EXE +coredumpctl --json=short | jq +coredumpctl --json=pretty | jq +coredumpctl --json=off +coredumpctl --root=/ +coredumpctl --directory=/var/log/journal +coredumpctl --file="/var/log/journal/$(</etc/machine-id)"/*.journal +coredumpctl --since=@0 +coredumpctl --since=yesterday --until=tomorrow +# We should have a couple of externally stored coredumps +coredumpctl --field=COREDUMP_FILENAME | tee /tmp/coredumpctl.out +grep "/var/lib/systemd/coredump/core" /tmp/coredumpctl.out +rm -f /tmp/coredumpctl.out + +coredumpctl info +coredumpctl info "$CORE_TEST_BIN" +coredumpctl info /foo /bar/ /baz "$CORE_TEST_BIN" +coredumpctl info "${CORE_TEST_BIN##*/}" +coredumpctl info foo bar baz "${CORE_TEST_BIN##*/}" +coredumpctl info COREDUMP_EXE="$CORE_TEST_BIN" +coredumpctl info COREDUMP_EXE=aaaaa COREDUMP_EXE= COREDUMP_EXE="$CORE_TEST_BIN" + +coredumpctl debug --debugger=/bin/true "$CORE_TEST_BIN" +SYSTEMD_DEBUGGER=/bin/true coredumpctl debug "$CORE_TEST_BIN" +coredumpctl debug --debugger=/bin/true --debugger-arguments="-this --does --not 'do anything' -a -t --all" "${CORE_TEST_BIN##*/}" + +coredumpctl dump "$CORE_TEST_BIN" >/tmp/core.redirected +test -s /tmp/core.redirected +coredumpctl dump -o /tmp/core.output "${CORE_TEST_BIN##*/}" +test -s /tmp/core.output +rm -f /tmp/core.{output,redirected} + +# Unprivileged stuff +# Related issue: https://github.com/systemd/systemd/issues/26912 +UNPRIV_CMD=(systemd-run --user --wait --pipe -M "testuser@.host" --) +# Trigger a couple of coredumps as an unprivileged user +"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGTRAP" +"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGABRT" +# In the tests we store the coredumps in journals, so let's generate a couple +# with Storage=external as well +mkdir -p /run/systemd/coredump.conf.d/ +printf '[Coredump]\nStorage=external' >/run/systemd/coredump.conf.d/99-external.conf +"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGTRAP" +"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGABRT" +rm -fv /run/systemd/coredump.conf.d/99-external.conf +# Wait a bit for the coredumps to get processed +timeout 30 bash -c "while [[ \$(coredumpctl list -q --no-legend $CORE_TEST_UNPRIV_BIN | wc -l) -lt 4 ]]; do sleep 1; done" + +# root should see coredumps from both binaries +coredumpctl info "$CORE_TEST_UNPRIV_BIN" +coredumpctl info "${CORE_TEST_UNPRIV_BIN##*/}" +# The test user should see only their own coredumps +"${UNPRIV_CMD[@]}" coredumpctl +"${UNPRIV_CMD[@]}" coredumpctl info "$CORE_TEST_UNPRIV_BIN" +"${UNPRIV_CMD[@]}" coredumpctl info "${CORE_TEST_UNPRIV_BIN##*/}" +(! "${UNPRIV_CMD[@]}" coredumpctl info --all "$CORE_TEST_BIN") +(! "${UNPRIV_CMD[@]}" coredumpctl info --all "${CORE_TEST_BIN##*/}") +# We should have a couple of externally stored coredumps +"${UNPRIV_CMD[@]}" coredumpctl --field=COREDUMP_FILENAME | tee /tmp/coredumpctl.out +grep "/var/lib/systemd/coredump/core" /tmp/coredumpctl.out +rm -f /tmp/coredumpctl.out + +"${UNPRIV_CMD[@]}" coredumpctl debug --debugger=/bin/true "$CORE_TEST_UNPRIV_BIN" +"${UNPRIV_CMD[@]}" coredumpctl debug --debugger=/bin/true --debugger-arguments="-this --does --not 'do anything' -a -t --all" "${CORE_TEST_UNPRIV_BIN##*/}" + +"${UNPRIV_CMD[@]}" coredumpctl dump "$CORE_TEST_UNPRIV_BIN" >/tmp/core.redirected +test -s /tmp/core.redirected +"${UNPRIV_CMD[@]}" coredumpctl dump -o /tmp/core.output "${CORE_TEST_UNPRIV_BIN##*/}" +test -s /tmp/core.output +rm -f /tmp/core.{output,redirected} +(! "${UNPRIV_CMD[@]}" coredumpctl dump "$CORE_TEST_BIN" >/dev/null) + +# --backtrace mode +# Pass one of the existing journal coredump records to systemd-coredump and +# use our PID as the source to make matching the coredump later easier +# systemd-coredump args: PID UID GID SIGNUM TIMESTAMP CORE_SOFT_RLIMIT HOSTNAME +journalctl -b -n 1 --output=export --output-fields=MESSAGE,COREDUMP COREDUMP_EXE="/usr/bin/test-dump" | + /usr/lib/systemd/systemd-coredump --backtrace $$ 0 0 6 1679509994 12345 mymachine +# Wait a bit for the coredump to get processed +timeout 30 bash -c "while [[ \$(coredumpctl list -q --no-legend $$ | wc -l) -eq 0 ]]; do sleep 1; done" +coredumpctl info "$$" +coredumpctl info COREDUMP_HOSTNAME="mymachine" + +# This used to cause a stack overflow +systemd-run -t --property CoredumpFilter=all ls /tmp +systemd-run -t --property CoredumpFilter=default ls /tmp + +(! coredumpctl --hello-world) +(! coredumpctl -n 0) +(! coredumpctl -n -1) +(! coredumpctl --file=/dev/null) +(! coredumpctl --since=0) +(! coredumpctl --until='') +(! coredumpctl --since=today --until=yesterday) +(! coredumpctl --directory=/ --root=/) +(! coredumpctl --json=foo) +(! coredumpctl -F foo -F bar) +(! coredumpctl list 0) +(! coredumpctl list -- -1) +(! coredumpctl list '') +(! coredumpctl info /../.~=) +(! coredumpctl info '') +(! coredumpctl dump --output=/dev/full "$CORE_TEST_BIN") +(! coredumpctl dump --output=/dev/null --output=/dev/null "$CORE_TEST_BIN") +(! coredumpctl debug --debugger=/bin/false) +(! coredumpctl debug --debugger=/bin/true --debugger-arguments='"') diff --git a/test/units/testsuite-74.delta.sh b/test/units/testsuite-74.delta.sh new file mode 100755 index 0000000..a0e1cb5 --- /dev/null +++ b/test/units/testsuite-74.delta.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +at_exit() { + rm -rfv /{run,etc}/systemd/system/delta-test* +} + +trap at_exit EXIT + +# Create a couple of supporting units with overrides +# +# Extended unit +cat >"/run/systemd/system/delta-test-unit-extended.service" <<EOF +[Service] +ExecStart=/bin/true +EOF +mkdir -p "/run/systemd/system/delta-test-unit-extended.service.d" +cat >"/run/systemd/system/delta-test-unit-extended.service.d/override.conf" <<EOF +[Unit] +Description=Foo Bar +[Service] +ExecStartPre=/bin/true +EOF +# Masked unit +cp -fv /run/systemd/system/delta-test-unit-extended.service /run/systemd/system/delta-test-unit-masked.service +systemctl mask delta-test-unit-masked.service +# Overridden unit +cp -fv /run/systemd/system/delta-test-unit-extended.service /run/systemd/system/delta-test-unit-overridden.service +cp -fv /run/systemd/system/delta-test-unit-overridden.service /etc/systemd/system/delta-test-unit-overridden.service +echo "ExecStartPost=/bin/true" >>/etc/systemd/system/delta-test-unit-overridden.service +# Overridden but equivalent unit +ln -srfv /run/systemd/system/delta-test-unit-extended.service /run/systemd/system/delta-test-unit-equivalent.service +ln -sfv /run/systemd/system/delta-test-unit-extended.service /etc/systemd/system/delta-test-unit-equivalent.service +# Redirected unit +ln -srfv /run/systemd/system/delta-test-unit-extended.service /run/systemd/system/delta-test-unit-redirected.service +ln -sfv /run/systemd/system/delta-test-unit-overidden.service /etc/systemd/system/delta-test-unit-extended.service + +systemctl daemon-reload + +systemd-delta +systemd-delta /run +systemd-delta systemd/system +systemd-delta /run systemd/system /run +systemd-delta /run foo/bar hello/world systemd/system /run +systemd-delta foo/bar +systemd-delta --diff=true +systemd-delta --diff=false + +for type in masked equivalent redirected overridden extended unchanged; do + systemd-delta --type="$type" + systemd-delta --type="$type" /run +done +systemd-delta --type=equivalent,redirected + +(! systemd-delta --diff=foo) +(! systemd-delta --type=foo) +(! systemd-delta --type=equivalent,redirected,foo) diff --git a/test/units/testsuite-74.escape.sh b/test/units/testsuite-74.escape.sh new file mode 100755 index 0000000..e398d40 --- /dev/null +++ b/test/units/testsuite-74.escape.sh @@ -0,0 +1,108 @@ +#!/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 + +# Simple wrapper to check both escaping and unescaping of given strings +# Arguments: +# $1 - expected unescaped string +# $2 - expected escaped string +# $3 - optional arguments for systemd-escape +check_escape() { + unescaped="${1?}" + escaped="${2?}" + shift 2 + + assert_eq "$(systemd-escape "$@" -- "$unescaped")" "$escaped" + assert_eq "$(systemd-escape "$@" --unescape -- "$escaped")" "$unescaped" +} + +systemd-escape --help +systemd-escape --version + +check_escape '' '' +check_escape 'hello' 'hello' +check_escape 'hello-world' 'hello\x2dworld' +check_escape '-+ěščřž---🤔' '\x2d\x2b\xc4\x9b\xc5\xa1\xc4\x8d\xc5\x99\xc5\xbe\x2d\x2d\x2d\xf0\x9f\xa4\x94' +check_escape '/this/is/a/path/a b c' '-this-is-a-path-a\x20b\x20c' + +# Multiple strings to escape/unescape +assert_eq "$(systemd-escape 'hello-world' '/dev/loop1' 'template@🐍')" \ + 'hello\x2dworld -dev-loop1 template\x40\xf0\x9f\x90\x8d' +assert_eq "$(systemd-escape --unescape -- 'hello\x2dworld' '-dev-loop1' 'template\x40\xf0\x9f\x90\x8d')" \ + 'hello-world /dev/loop1 template@🐍' + +# --suffix= is not compatible with --unescape +assert_eq "$(systemd-escape --suffix=mount -- '-+ěščřž---🤔')" \ + '\x2d\x2b\xc4\x9b\xc5\xa1\xc4\x8d\xc5\x99\xc5\xbe\x2d\x2d\x2d\xf0\x9f\xa4\x94.mount' +assert_eq "$(systemd-escape --suffix=timer 'this has spaces')" \ + 'this\x20has\x20spaces.timer' +assert_eq "$(systemd-escape --suffix=service 'trailing-spaces ')" \ + 'trailing\x2dspaces\x20\x20.service' +assert_eq "$(systemd-escape --suffix=automount ' leading-spaces')" \ + '\x20\x20\x20leading\x2dspaces.automount' + +# --template= +check_escape 'hello' 'hello@hello.service' --template=hello@.service +check_escape ' what - is _ love? 🤔 ¯\_(ツ)_/¯' \ + 'hello@\x20\x20what\x20\x2d\x20is\x20_\x20love\x3f\x20\xf0\x9f\xa4\x94\x20\xc2\xaf\x5c_\x28\xe3\x83\x84\x29_-\xc2\xaf.service' \ + --template=hello@.service +check_escape '/this/is/where/my/stuff/is/ with spaces though ' \ + 'mount-my-stuff@-this-is-where-my-stuff-is-\x20with\x20spaces\x20though\x20.service' \ + --template=mount-my-stuff@.service +check_escape '/this/is/where/my/stuff/is/ with spaces though ' \ + 'mount-my-stuff@this-is-where-my-stuff-is-\x20with\x20spaces\x20though\x20.service' \ + --template=mount-my-stuff@.service --path + +# --instance (must be used with --unescape) +assert_eq "$(systemd-escape --unescape --instance 'hello@\x20\x20what\x20\x2d\x20is\x20_\x20love\x3f\x20\xf0\x9f\xa4\x94\x20\xc2\xaf\x5c_\x28\xe3\x83\x84\x29_-\xc2\xaf.service')" \ + ' what - is _ love? 🤔 ¯\_(ツ)_/¯' +assert_eq "$(systemd-escape --unescape --instance 'mount-my-stuff@-this-is-where-my-stuff-is-\x20with\x20spaces\x20though\x20.service')" \ + '/this/is/where/my/stuff/is/ with spaces though ' +assert_eq "$(systemd-escape --unescape --instance --path 'mount-my-stuff@this-is-where-my-stuff-is-\x20with\x20spaces\x20though\x20.service')" \ + '/this/is/where/my/stuff/is/ with spaces though ' + +# --path, reversible cases +check_escape / '-' --path +check_escape '/hello/world' 'hello-world' --path +check_escape '/mnt/smb/おにぎり' \ + 'mnt-smb-\xe3\x81\x8a\xe3\x81\xab\xe3\x81\x8e\xe3\x82\x8a' \ + --path + +# --path, non-reversible cases +assert_eq "$(systemd-escape --path ///////////////)" '-' +assert_eq "$(systemd-escape --path /..)" '-' +assert_eq "$(systemd-escape --path /../.././../.././)" '-' +assert_eq "$(systemd-escape --path /../.././../.././foo)" 'foo' + +# --mangle +assert_eq "$(systemd-escape --mangle 'hello-world')" 'hello-world.service' +assert_eq "$(systemd-escape --mangle '/mount/this')" 'mount-this.mount' +assert_eq "$(systemd-escape --mangle 'my-service@ 🐱 ')" 'my-service@\x20\xf0\x9f\x90\xb1\x20.service' +assert_eq "$(systemd-escape --mangle '/dev/disk/by-emoji/🍎')" 'dev-disk-by\x2demoji-\xf0\x9f\x8d\x8e.device' +assert_eq "$(systemd-escape --mangle 'daily-existential-crisis .timer')" 'daily-existential-crisis\x20.timer' +assert_eq "$(systemd-escape --mangle 'trailing-whitespace.mount ')" 'trailing-whitespace.mount\x20.service' + +(! systemd-escape) +(! systemd-escape --suffix='' hello) +(! systemd-escape --suffix=invalid hello) +(! systemd-escape --suffix=mount --template=hello@.service hello) +(! systemd-escape --suffix=mount --mangle) +(! systemd-escape --template='') +(! systemd-escape --template=@) +(! systemd-escape --template='hello@.service' '') +(! systemd-escape --unescape --template='hello@.service' '@hello.service') +(! systemd-escape --unescape --template='hello@.service' 'hello@.service') +(! systemd-escape --mangle --template=hello@.service hello) +(! systemd-escape --instance 'hello@hello.service') +(! systemd-escape --instance --template=hello@.service 'hello@hello.service') +(! systemd-escape --unescape --instance --path 'mount-my-stuff@-this-is-where-my-stuff-is-\x20with\x20spaces\x20though\x20.service') +(! systemd-escape --path '/../hello/..') +(! systemd-escape --path '.') +(! systemd-escape --path '..') +(! systemd-escape --path "$(set +x; printf '%0.sa' {0..256})") +(! systemd-escape --unescape --path '') +(! systemd-escape --mangle '') diff --git a/test/units/testsuite-74.firstboot.sh b/test/units/testsuite-74.firstboot.sh new file mode 100755 index 0000000..be08575 --- /dev/null +++ b/test/units/testsuite-74.firstboot.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if ! command -v systemd-firstboot >/dev/null; then + echo "systemd-firstboot not found, skipping the test" + exit 0 +fi + +at_exit() { + if [[ -n "${ROOT:-}" ]]; then + ls -lR "$ROOT" + rm -fr "$ROOT" + fi +} + +trap at_exit EXIT + +# Generated via `mkpasswd -m sha-512 -S foobarsalt password1` +# shellcheck disable=SC2016 +ROOT_HASHED_PASSWORD1='$6$foobarsalt$YbwdaATX6IsFxvWbY3QcZj2gB31R/LFRFrjlFrJtTTqFtSfn4dfOAg/km2k4Sl.a2g7LOYDo31wMTaEsCo9j41' +# Generated via `mkpasswd -m sha-512 -S foobarsalt password2` +# shellcheck disable=SC2016 +ROOT_HASHED_PASSWORD2='$6$foobarsalt$q.P2932zYMLbKnjFwIxPI8y3iuxeuJ2BgE372LcZMMnj3Gcg/9mJg2LPKUl.ha0TG/.fRNNnRQcLfzM0SNot3.' + +# Debian and Ubuntu use /etc/default/locale instead of /etc/locale.conf. Make +# sure we use the appropriate path for locale configuration. +LOCALE_PATH="/etc/locale.conf" +[ -e "$LOCALE_PATH" ] || LOCALE_PATH="/etc/default/locale" +[ -e "$LOCALE_PATH" ] || systemd-firstboot --locale=C.UTF-8 + +# Create a minimal root so we don't modify the testbed +ROOT=test-root +mkdir -p "$ROOT/bin" +# Dummy shell for --root-shell= +touch "$ROOT/bin/fooshell" "$ROOT/bin/barshell" + +systemd-firstboot --root="$ROOT" --locale=foo +grep -q "LANG=foo" "$ROOT$LOCALE_PATH" +rm -fv "$ROOT$LOCALE_PATH" +systemd-firstboot --root="$ROOT" --locale-messages=foo +grep -q "LC_MESSAGES=foo" "$ROOT$LOCALE_PATH" +rm -fv "$ROOT$LOCALE_PATH" +systemd-firstboot --root="$ROOT" --locale=foo --locale-messages=bar +grep -q "LANG=foo" "$ROOT$LOCALE_PATH" +grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" + +systemd-firstboot --root="$ROOT" --keymap=foo +grep -q "KEYMAP=foo" "$ROOT/etc/vconsole.conf" + +systemd-firstboot --root="$ROOT" --timezone=Europe/Berlin +readlink "$ROOT/etc/localtime" | grep -q "Europe/Berlin" + +systemd-firstboot --root="$ROOT" --hostname "foobar" +grep -q "foobar" "$ROOT/etc/hostname" + +systemd-firstboot --root="$ROOT" --machine-id=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +grep -q "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "$ROOT/etc/machine-id" + +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" +systemd-firstboot --root="$ROOT" --root-password=foo +grep -q "^root:x:0:0:" "$ROOT/etc/passwd" +grep -q "^root:" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" +echo "foo" >root.passwd +systemd-firstboot --root="$ROOT" --root-password-file=root.passwd +grep -q "^root:x:0:0:" "$ROOT/etc/passwd" +grep -q "^root:" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" root.passwd +# Set the shell together with the password, as firstboot won't touch +# /etc/passwd if it already exists +systemd-firstboot --root="$ROOT" --root-password-hashed="$ROOT_HASHED_PASSWORD1" --root-shell=/bin/fooshell +grep -q "^root:x:0:0:.*:/bin/fooshell$" "$ROOT/etc/passwd" +grep -q "^root:$ROOT_HASHED_PASSWORD1:" "$ROOT/etc/shadow" + +systemd-firstboot --root="$ROOT" --kernel-command-line="foo.bar=42" +grep -q "foo.bar=42" "$ROOT/etc/kernel/cmdline" + +# Configs should not get overwritten if they exist unless --force is used +systemd-firstboot --root="$ROOT" \ + --locale=locale-overwrite \ + --locale-messages=messages-overwrite \ + --keymap=keymap-overwrite \ + --timezone=CET \ + --hostname=hostname-overwrite \ + --machine-id=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ + --root-password-hashed="$ROOT_HASHED_PASSWORD2" \ + --root-shell=/bin/barshell \ + --kernel-command-line="hello.world=0" +grep -q "LANG=foo" "$ROOT$LOCALE_PATH" +grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" +grep -q "KEYMAP=foo" "$ROOT/etc/vconsole.conf" +readlink "$ROOT/etc/localtime" | grep -q "Europe/Berlin$" +grep -q "foobar" "$ROOT/etc/hostname" +grep -q "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "$ROOT/etc/machine-id" +grep -q "^root:x:0:0:.*:/bin/fooshell$" "$ROOT/etc/passwd" +grep -q "^root:$ROOT_HASHED_PASSWORD1:" "$ROOT/etc/shadow" +grep -q "foo.bar=42" "$ROOT/etc/kernel/cmdline" + +# The same thing, but now with --force +systemd-firstboot --root="$ROOT" --force \ + --locale=locale-overwrite \ + --locale-messages=messages-overwrite \ + --keymap=keymap-overwrite \ + --timezone=CET \ + --hostname=hostname-overwrite \ + --machine-id=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ + --root-password-hashed="$ROOT_HASHED_PASSWORD2" \ + --root-shell=/bin/barshell \ + --kernel-command-line="hello.world=0" +grep -q "LANG=locale-overwrite" "$ROOT$LOCALE_PATH" +grep -q "LC_MESSAGES=messages-overwrite" "$ROOT$LOCALE_PATH" +grep -q "KEYMAP=keymap-overwrite" "$ROOT/etc/vconsole.conf" +readlink "$ROOT/etc/localtime" | grep -q "/CET$" +grep -q "hostname-overwrite" "$ROOT/etc/hostname" +grep -q "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "$ROOT/etc/machine-id" +grep -q "^root:x:0:0:.*:/bin/barshell$" "$ROOT/etc/passwd" +grep -q "^root:$ROOT_HASHED_PASSWORD2:" "$ROOT/etc/shadow" +grep -q "hello.world=0" "$ROOT/etc/kernel/cmdline" + +# Test that --reset removes all files configured by firstboot. +systemd-firstboot --root="$ROOT" --reset +[[ ! -e "$ROOT/etc/locale.conf" ]] +[[ ! -e "$ROOT/etc/vconsole.conf" ]] +[[ ! -e "$ROOT/etc/localtime" ]] +[[ ! -e "$ROOT/etc/hostname" ]] +[[ ! -e "$ROOT/etc/machine-id" ]] +[[ ! -e "$ROOT/etc/kernel/cmdline" ]] + +# --copy-* options +rm -fr "$ROOT" +mkdir "$ROOT" +# Copy everything at once (--copy) +systemd-firstboot --root="$ROOT" --copy +diff $LOCALE_PATH "$ROOT$LOCALE_PATH" +diff <(awk -F: '/^root/ { print $7; }' /etc/passwd) <(awk -F: '/^root/ { print $7; }' "$ROOT/etc/passwd") +diff <(awk -F: '/^root/ { print $2; }' /etc/shadow) <(awk -F: '/^root/ { print $2; }' "$ROOT/etc/shadow") +[[ -e /etc/vconsole.conf ]] && diff /etc/vconsole.conf "$ROOT/etc/vconsole.conf" +[[ -e /etc/localtime ]] && diff <(readlink /etc/localtime) <(readlink "$ROOT/etc/localtime") +rm -fr "$ROOT" +mkdir "$ROOT" +# Copy everything at once, but now by using separate switches +systemd-firstboot --root="$ROOT" --copy-locale --copy-keymap --copy-timezone --copy-root-password --copy-root-shell +diff $LOCALE_PATH "$ROOT$LOCALE_PATH" +diff <(awk -F: '/^root/ { print $7; }' /etc/passwd) <(awk -F: '/^root/ { print $7; }' "$ROOT/etc/passwd") +diff <(awk -F: '/^root/ { print $2; }' /etc/shadow) <(awk -F: '/^root/ { print $2; }' "$ROOT/etc/shadow") +[[ -e /etc/vconsole.conf ]] && diff /etc/vconsole.conf "$ROOT/etc/vconsole.conf" +[[ -e /etc/localtime ]] && diff <(readlink /etc/localtime) <(readlink "$ROOT/etc/localtime") + +# --prompt-* options +rm -fr "$ROOT" +mkdir -p "$ROOT/bin" +touch "$ROOT/bin/fooshell" "$ROOT/bin/barshell" +# Temporarily disable pipefail to avoid `echo: write error: Broken pipe +set +o pipefail +# We can do only limited testing here, since it's all an interactive stuff, +# so --prompt and --prompt-root-password are skipped on purpose +echo -ne "\nfoo\nbar\n" | systemd-firstboot --root="$ROOT" --prompt-locale +grep -q "LANG=foo" "$ROOT$LOCALE_PATH" +grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" +# systemd-firstboot in prompt-keymap mode requires keymaps to be installed so +# it can present them as a list to the user. As Debian does not ship/provide +# compatible keymaps (from the kbd package), skip this test if the keymaps are +# missing. +if [ -d "/usr/share/keymaps/" ] || [ -d "/usr/share/kbd/keymaps/" ] || [ -d "/usr/lib/kbd/keymaps/" ] ; then + echo -ne "\nfoo\n" | systemd-firstboot --root="$ROOT" --prompt-keymap + grep -q "KEYMAP=foo" "$ROOT/etc/vconsole.conf" +fi +echo -ne "\nEurope/Berlin\n" | systemd-firstboot --root="$ROOT" --prompt-timezone +readlink "$ROOT/etc/localtime" | grep -q "Europe/Berlin$" +echo -ne "\nfoobar\n" | systemd-firstboot --root="$ROOT" --prompt-hostname +grep -q "foobar" "$ROOT/etc/hostname" +echo -ne "\n/bin/fooshell\n" | systemd-firstboot --root="$ROOT" --prompt-root-shell +grep -q "^root:.*:0:0:.*:/bin/fooshell$" "$ROOT/etc/passwd" +# Existing files should not get overwritten +echo -ne "\n/bin/barshell\n" | systemd-firstboot --root="$ROOT" --prompt-root-shell +grep -q "^root:.*:0:0:.*:/bin/fooshell$" "$ROOT/etc/passwd" +# Now without the welcome screen but with force +echo -ne "/bin/barshell\n" | systemd-firstboot --root="$ROOT" --force --prompt-root-shell --welcome=no +grep -q "^root:.*:0:0:.*:/bin/barshell$" "$ROOT/etc/passwd" +# Re-enable pipefail +set -o pipefail + +# Assorted tests +rm -fr "$ROOT" +mkdir "$ROOT" + +systemd-firstboot --root="$ROOT" --setup-machine-id +grep -E "[a-z0-9]{32}" "$ROOT/etc/machine-id" + +systemd-firstboot --root="$ROOT" --delete-root-password +diff <(echo) <(awk -F: '/^root/ { print $2; }' "$ROOT/etc/shadow") + +(! systemd-firstboot --root="$ROOT" --root-shell=/bin/nonexistentshell) +(! systemd-firstboot --root="$ROOT" --machine-id=invalidmachineid) +(! systemd-firstboot --root="$ROOT" --timezone=Foo/Bar) diff --git a/test/units/testsuite-74.id128.sh b/test/units/testsuite-74.id128.sh new file mode 100755 index 0000000..c1b80d6 --- /dev/null +++ b/test/units/testsuite-74.id128.sh @@ -0,0 +1,50 @@ +#!/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 + +systemd-id128 --help +systemd-id128 help +systemd-id128 show +systemd-id128 show --pretty | tail +systemd-id128 show --value | tail +systemd-id128 show 4f68bce3e8cd4db196e7fbcaf984b709 # root-x86-64 +systemd-id128 show --pretty 4f68bce3e8cd4db196e7fbcaf984b709 +systemd-id128 show root-x86-64 +systemd-id128 show --pretty root-x86-64 +[[ "$(systemd-id128 show 4f68bce3e8cd4db196e7fbcaf984b709)" = "$(systemd-id128 show root-x86-64)" ]] +[[ "$(systemd-id128 show 4f68bce3-e8cd-4db1-96e7-fbcaf984b709)" = "$(systemd-id128 show root-x86-64)" ]] + +systemd-id128 show root-x86-64 --app-specific=4f68bce3e8cd4db196e7fbcaf984b709 +systemd-id128 show --pretty root-x86-64 --app-specific=4f68bce3e8cd4db196e7fbcaf984b709 +[[ "$(systemd-id128 show root-x86-64 --app-specific=4f68bce3e8cd4db196e7fbcaf984b709 -P)" = "8ee5535e7cb14c249e1d28b8dfbb939c" ]] + +[[ "$(systemd-id128 new | wc -c)" -eq 33 ]] +systemd-id128 new -p +systemd-id128 new -u +systemd-id128 new -a 4f68bce3e8cd4db196e7fbcaf984b709 + +systemd-id128 machine-id +systemd-id128 machine-id --pretty +systemd-id128 machine-id --uuid +systemd-id128 machine-id --app-specific=4f68bce3e8cd4db196e7fbcaf984b709 +assert_eq "$(systemd-id128 machine-id)" "$(</etc/machine-id)" + +systemd-id128 boot-id +systemd-id128 boot-id --pretty +systemd-id128 boot-id --uuid +systemd-id128 boot-id --app-specific=4f68bce3e8cd4db196e7fbcaf984b709 +assert_eq "$(systemd-id128 boot-id --uuid)" "$(</proc/sys/kernel/random/boot_id)" + +# shellcheck disable=SC2016 +systemd-run --wait --pipe bash -euxc '[[ $INVOCATION_ID == "$(systemd-id128 invocation-id)" ]]' + +(! systemd-id128) +(! systemd-id128 new -a '') +(! systemd-id128 new -a '0') +(! systemd-id128 invocation-id -a 4f68bce3e8cd4db196e7fbcaf984b709) +(! systemd-id128 show '') +(! systemd-id128 show "$(set +x; printf '%0.s0' {0..64})") diff --git a/test/units/testsuite-74.machine-id-setup.sh b/test/units/testsuite-74.machine-id-setup.sh new file mode 100755 index 0000000..c2b9db5 --- /dev/null +++ b/test/units/testsuite-74.machine-id-setup.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2064 +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 + +root_mock() { + local root="${1:?}" + + mkdir -p "$root" + # Put a tmpfs over the "root", so we're able to remount it as read-only + # when needed + mount -t tmpfs tmpfs "$root" + mkdir "$root/etc" "$root/run" +} + +root_cleanup() { + local root="${1:?}" + + umount --recursive "$root" + rm -fr "$root" +} + +testcase_sanity() { + systemd-machine-id-setup + systemd-machine-id-setup --help + systemd-machine-id-setup --version + systemd-machine-id-setup --print + systemd-machine-id-setup --root= --print + systemd-machine-id-setup --root=/ --print + + (! systemd-machine-id-setup "") + (! systemd-machine-id-setup --foo) +} + +testcase_invalid() { + local root machine_id + + root="$(mktemp -d)" + trap "root_cleanup $root" RETURN + root_mock "$root" + + systemd-machine-id-setup --print --root "$root" + echo abc >>"$root/etc/machine-id" + machine_id="$(systemd-machine-id-setup --print --root "$root")" + diff <(echo "$machine_id") "$root/etc/machine-id" +} + +testcase_transient() { + local root transient_id committed_id + + root="$(mktemp -d)" + trap "root_cleanup $root" RETURN + root_mock "$root" + + systemd-machine-id-setup --print --root "$root" + echo abc >>"$root/etc/machine-id" + mount -o remount,ro "$root" + mount -t tmpfs tmpfs "$root/run" + transient_id="$(systemd-machine-id-setup --print --root "$root")" + mount -o remount,rw "$root" + committed_id="$(systemd-machine-id-setup --print --commit --root "$root")" + [[ "$transient_id" == "$committed_id" ]] + diff "$root/etc/machine-id" "$root/run/machine-id" +} + +# Check if we correctly processed the invalid machine ID we set up in the respective +# test.sh file +systemctl --state=failed --no-legend --no-pager >/failed +test ! -s /failed + +run_testcases diff --git a/test/units/testsuite-74.modules-load.sh b/test/units/testsuite-74.modules-load.sh new file mode 100755 index 0000000..3d00e07 --- /dev/null +++ b/test/units/testsuite-74.modules-load.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +MODULES_LOAD_BIN="/usr/lib/systemd/systemd-modules-load" +CONFIG_FILE="/run/modules-load.d/99-test.conf" + +at_exit() { + rm -rfv "${CONFIG_FILE:?}" +} + +trap at_exit EXIT + +if systemd-detect-virt -cq; then + echo "Running in a container, skipping the systemd-modules-load test..." + exit 0 +fi + +# Check if we have required kernel modules +modprobe --all --resolve-alias loop dummy + +mkdir -p /run/modules-load.d/ + +"$MODULES_LOAD_BIN" +"$MODULES_LOAD_BIN" --help +"$MODULES_LOAD_BIN" --version + +# Explicit config file +modprobe -v --all --remove loop dummy +printf "loop\ndummy" >"$CONFIG_FILE" +"$MODULES_LOAD_BIN" "$CONFIG_FILE" |& tee /tmp/out.log +grep -E "Inserted module .*loop" /tmp/out.log +grep -E "Inserted module .*dummy" /tmp/out.log + +# Implicit config file +modprobe -v --all --remove loop dummy +printf "loop\ndummy" >"$CONFIG_FILE" +"$MODULES_LOAD_BIN" |& tee /tmp/out.log +grep -E "Inserted module .*loop" /tmp/out.log +grep -E "Inserted module .*dummy" /tmp/out.log + +# Valid & invalid data mixed together +modprobe -v --all --remove loop dummy +cat >"$CONFIG_FILE" <<EOF + +loop +loop +loop + loop +dummy + \\n\n\n\\\\\\ + +loo!@@123##2455 +# This is a comment +$(printf "%.0sx" {0..4096}) +dummy +loop +foo-bar-baz +1 +" +' +EOF +"$MODULES_LOAD_BIN" |& tee /tmp/out.log +grep -E "^Inserted module .*loop" /tmp/out.log +grep -E "^Inserted module .*dummy" /tmp/out.log +grep -E "^Failed to find module .*foo-bar-baz" /tmp/out.log +(! grep -E "This is a comment" /tmp/out.log) +# Each module should be loaded only once, even if specified multiple times +[[ "$(grep -Ec "^Inserted module" /tmp/out.log)" -eq 2 ]] +[[ "$(grep -Ec "^Failed to find module" /tmp/out.log)" -eq 7 ]] + +# Command line arguments +modprobe -v --all --remove loop dummy +# Make sure we have no config files left over that might interfere with +# following tests +rm -fv "$CONFIG_FILE" +[[ -z "$(systemd-analyze cat-config modules-load.d)" ]] +CMDLINE="ro root= modules_load= modules_load=, / = modules_load=foo-bar-baz,dummy modules_load=loop,loop,loop" +SYSTEMD_PROC_CMDLINE="$CMDLINE" "$MODULES_LOAD_BIN" |& tee /tmp/out.log +grep -E "^Inserted module .*loop" /tmp/out.log +grep -E "^Inserted module .*dummy" /tmp/out.log +grep -E "^Failed to find module .*foo-bar-baz" /tmp/out.log +# Each module should be loaded only once, even if specified multiple times +[[ "$(grep -Ec "^Inserted module" /tmp/out.log)" -eq 2 ]] + +(! "$MODULES_LOAD_BIN" --nope) +(! "$MODULES_LOAD_BIN" /foo/bar/baz) diff --git a/test/units/testsuite-74.mount.sh b/test/units/testsuite-74.mount.sh new file mode 100755 index 0000000..41c5c86 --- /dev/null +++ b/test/units/testsuite-74.mount.sh @@ -0,0 +1,151 @@ +#!/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 + +# We're going to play around with block/loop devices, so bail out early +# if we're running in nspawn +if systemd-detect-virt --container >/dev/null; then + echo "Container detected, skipping the test" + exit 0 +fi + +at_exit() { + set +e + + [[ -n "${LOOP:-}" ]] && losetup -d "$LOOP" + [[ -n "${WORK_DIR:-}" ]] && rm -fr "$WORK_DIR" +} + +trap at_exit EXIT + +WORK_DIR="$(mktemp -d)" + +systemd-mount --list +systemd-mount --list --full +systemd-mount --list --no-legend +systemd-mount --list --no-pager +systemd-mount --list --quiet + +# Set up a simple block device for further tests +dd if=/dev/zero of="$WORK_DIR/simple.img" bs=1M count=16 +LOOP="$(losetup --show --find "$WORK_DIR/simple.img")" +mkfs.ext4 -L sd-mount-test "$LOOP" +mkdir "$WORK_DIR/mnt" +mount "$LOOP" "$WORK_DIR/mnt" +touch "$WORK_DIR/mnt/foo.bar" +umount "$LOOP" +(! mountpoint "$WORK_DIR/mnt") + +# Mount with both source and destination set +systemd-mount "$LOOP" "$WORK_DIR/mnt" +systemctl status "$WORK_DIR/mnt" +systemd-mount --list --full +test -e "$WORK_DIR/mnt/foo.bar" +systemd-umount "$WORK_DIR/mnt" +# Same thing, but with explicitly specified filesystem and disabled filesystem check +systemd-mount --type=ext4 --fsck=no --collect "$LOOP" "$WORK_DIR/mnt" +systemctl status "$(systemd-escape --path "$WORK_DIR/mnt").mount" +test -e "$WORK_DIR/mnt/foo.bar" +systemd-mount --umount "$LOOP" +# Discover additional metadata (unit description should now contain filesystem label) +systemd-mount --no-ask-password --discover "$LOOP" "$WORK_DIR/mnt" +test -e "$WORK_DIR/mnt/foo.bar" +systemctl show -P Description "$WORK_DIR/mnt" | grep -q sd-mount-test +systemd-umount "$WORK_DIR/mnt" +# Set a unit description +systemd-mount --description="Very Important Unit" "$LOOP" "$WORK_DIR/mnt" +test -e "$WORK_DIR/mnt/foo.bar" +systemctl show -P Description "$WORK_DIR/mnt" | grep -q "Very Important Unit" +systemd-umount "$WORK_DIR/mnt" +# Set a property +systemd-mount --property="Description=Foo Bar" "$LOOP" "$WORK_DIR/mnt" +test -e "$WORK_DIR/mnt/foo.bar" +systemctl show -P Description "$WORK_DIR/mnt" | grep -q "Foo Bar" +systemd-umount "$WORK_DIR/mnt" +# Set mount options +systemd-mount --options=ro,x-foo-bar "$LOOP" "$WORK_DIR/mnt" +test -e "$WORK_DIR/mnt/foo.bar" +systemctl show -P Options "$WORK_DIR/mnt" | grep -Eq "(^ro|,ro)" +systemctl show -P Options "$WORK_DIR/mnt" | grep -q "x-foo-bar" +systemd-umount "$WORK_DIR/mnt" + +# Mount with only source set +systemd-mount "$LOOP" +systemctl status /run/media/system/sd-mount-test +systemd-mount --list --full +test -e /run/media/system/sd-mount-test/foo.bar +systemd-umount LABEL=sd-mount-test + +# Automount +systemd-mount --automount=yes "$LOOP" "$WORK_DIR/mnt" +systemd-mount --list --full +systemctl status "$(systemd-escape --path "$WORK_DIR/mnt").automount" +[[ "$(systemctl show -P ActiveState "$WORK_DIR/mnt")" == inactive ]] +test -e "$WORK_DIR/mnt/foo.bar" +systemctl status "$WORK_DIR/mnt" +systemd-umount "$WORK_DIR/mnt" +# Automount + automount-specific property +systemd-mount -A --automount-property="Description=Bar Baz" "$LOOP" "$WORK_DIR/mnt" +systemctl show -P Description "$(systemd-escape --path "$WORK_DIR/mnt").automount" | grep -q "Bar Baz" +test -e "$WORK_DIR/mnt/foo.bar" +# Call --umount via --machine=, first with a relative path (bad) and then with +# an absolute one (good) +(! systemd-umount --machine=.host "$(realpath --relative-to=. "$WORK_DIR/mnt")") +systemd-umount --machine=.host "$WORK_DIR/mnt" + +# ext4 doesn't support uid=/gid= +(! systemd-mount -t ext4 --owner=testuser "$LOOP" "$WORK_DIR/mnt") + +# Automount + --bind-device +systemd-mount --automount=yes --bind-device --timeout-idle-sec=1 "$LOOP" "$WORK_DIR/mnt" +systemctl status "$(systemd-escape --path "$WORK_DIR/mnt").automount" +# Trigger the automount +test -e "$WORK_DIR/mnt/foo.bar" +# Wait until it's idle again +sleep 1.5 +# Safety net for slower/overloaded systems +timeout 10s bash -c "while systemctl is-active -q $WORK_DIR/mnt; do sleep .2; done" +systemctl status "$(systemd-escape --path "$WORK_DIR/mnt").automount" +# Disassemble the underlying block device +losetup -d "$LOOP" +unset LOOP +# The automount unit should disappear once the underlying blockdev is gone +timeout 10s bash -c "while systemctl status '$(systemd-escape --path "$WORK_DIR/mnt".automount)'; do sleep .2; done" + +# Mount a disk image +systemd-mount --discover "$WORK_DIR/simple.img" +# We can access files in the image even if the loopback block device is not initialized by udevd. +test -e /run/media/system/simple.img/foo.bar +# systemd-mount --list and systemd-umount require the loopback block device is initialized by udevd. +udevadm settle --timeout 30 +assert_in "/dev/loop.* ext4 +sd-mount-test" "$(systemd-mount --list --full)" +systemd-umount "$WORK_DIR/simple.img" + +# --owner + vfat +# +# Create a vfat image, as ext4 doesn't support uid=/gid= fixating for all +# files/directories +dd if=/dev/zero of="$WORK_DIR/owner-vfat.img" bs=1M count=16 +LOOP="$(losetup --show --find "$WORK_DIR/owner-vfat.img")" +mkfs.vfat -n owner-vfat "$LOOP" +# Mount it and check the UID/GID +[[ "$(stat -c "%U:%G" "$WORK_DIR/mnt")" == "root:root" ]] +systemd-mount --owner=testuser "$LOOP" "$WORK_DIR/mnt" +systemctl status "$WORK_DIR/mnt" +[[ "$(stat -c "%U:%G" "$WORK_DIR/mnt")" == "testuser:testuser" ]] +touch "$WORK_DIR/mnt/hello" +[[ "$(stat -c "%U:%G" "$WORK_DIR/mnt/hello")" == "testuser:testuser" ]] +systemd-umount LABEL=owner-vfat + +# tmpfs +mkdir -p "$WORK_DIR/mnt/foo/bar" +systemd-mount --tmpfs "$WORK_DIR/mnt/foo" +test ! -d "$WORK_DIR/mnt/foo/bar" +touch "$WORK_DIR/mnt/foo/baz" +systemd-umount "$WORK_DIR/mnt/foo" +test -d "$WORK_DIR/mnt/foo/bar" +test ! -e "$WORK_DIR/mnt/foo/baz" diff --git a/test/units/testsuite-74.networkctl.sh b/test/units/testsuite-74.networkctl.sh new file mode 100755 index 0000000..0a687af --- /dev/null +++ b/test/units/testsuite-74.networkctl.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +at_exit() { + systemctl stop systemd-networkd + + if [[ -v NETWORK_NAME && -v NETDEV_NAME && -v LINK_NAME ]]; then + rm -fvr {/usr/lib,/etc}/systemd/network/"$NETWORK_NAME" "/usr/lib/systemd/network/$NETDEV_NAME" \ + {/usr/lib,/etc}/systemd/network/"$LINK_NAME" "/etc/systemd/network/${NETWORK_NAME}.d" \ + "new" "+4" + fi +} + +trap at_exit EXIT + +export NETWORK_NAME="10-networkctl-test-$RANDOM.network" +export NETDEV_NAME="10-networkctl-test-$RANDOM.netdev" +export LINK_NAME="10-networkctl-test-$RANDOM.link" +cat >"/usr/lib/systemd/network/$NETWORK_NAME" <<EOF +[Match] +Name=test +EOF + +# Test files +networkctl cat "$NETWORK_NAME" | tail -n +2 | cmp - "/usr/lib/systemd/network/$NETWORK_NAME" + +cat >new <<EOF +[Match] +Name=test2 +EOF + +EDITOR='mv new' script -ec 'networkctl edit "$NETWORK_NAME"' /dev/null +printf '%s\n' '[Match]' 'Name=test2' | cmp - "/etc/systemd/network/$NETWORK_NAME" + +cat >"+4" <<EOF +[Network] +IPv6AcceptRA=no +EOF + +EDITOR='cp' script -ec 'networkctl edit "$NETWORK_NAME" --drop-in test' /dev/null +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") + +cat >"/usr/lib/systemd/network/$NETDEV_NAME" <<EOF +[NetDev] +Name=test2 +Kind=dummy +EOF + +networkctl cat "$NETDEV_NAME" + +cat >"/usr/lib/systemd/network/$LINK_NAME" <<EOF +[Match] +OriginalName=test2 + +[Link] +Alias=test_alias +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 +(! networkctl cat @test2) + +systemctl start systemd-networkd +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") + +EDITOR='cp' script -ec 'networkctl edit @test2 --drop-in test2.conf' /dev/null +cmp "+4" "/etc/systemd/network/${NETWORK_NAME}.d/test2.conf" + +ip_link="$(ip link show test2)" +if systemctl --quiet is-active systemd-udevd; then + assert_in 'alias test_alias' "$ip_link" +fi diff --git a/test/units/testsuite-74.path.sh b/test/units/testsuite-74.path.sh new file mode 100755 index 0000000..79056a5 --- /dev/null +++ b/test/units/testsuite-74.path.sh @@ -0,0 +1,89 @@ +#!/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 + +USER_DIRS_CONF="/root/.config/user-dirs.dirs" + +at_exit() { + set +e + + rm -fv "${USER_DIRS_CONF:?}" +} + +trap at_exit EXIT + +# Check that we indeed run under root to make the rest of the test work +[[ "$(id -u)" -eq 0 ]] + +# Create a custom user-dirs.dir file to exercise the xdg-user-dirs part +# of sd-path/from_user_dir() +mkdir -p "/root/.config" +cat >"${USER_DIRS_CONF:?}" <<\EOF +XDG_DESKTOP_DIR="$HOME/my-fancy-desktop" +XDG_INVALID + +XDG_DOWNLOAD_DIR = "$HOME" +XDG_TEMPLATES_DIR="/templates" +# Invalid records +XDG_TEMPLATES_DIR=/not-templates" +XDG_TEMPLATES_DIR="/also-not-teplates +XDG_TEMPLATES_DIR="" +XDG_TEMPLATES_DIR="../" + +XDG_PUBLICSHARE_DIR="$HOME/cat-pictures" +XDG_DOCUMENTS_DIR="$HOME/top/secret/documents" +XDG_MUSIC_DIR="/tmp/vaporwave" +XDG_PICTURES_DIR="$HOME/Pictures" +XDG_VIDEOS_DIR="$HOME/🤔" +EOF + +systemd-path --help +systemd-path --version +systemd-path +systemd-path temporary system-binaries user binfmt + +assert_eq "$(systemd-path system-runtime)" "/run" +assert_eq "$(systemd-path --suffix='' system-runtime)" "/run" +assert_eq "$(systemd-path --suffix='🤔' system-runtime)" "/run/🤔" +assert_eq "$(systemd-path --suffix=hello system-runtime)" "/run/hello" + +# Note for the stuff below: everything defaults to $HOME, only the desktop +# directory defaults to $HOME/Desktop. +# +# Check the user-dirs.dir stuff from above +assert_eq "$(systemd-path user)" "/root" +assert_eq "$(systemd-path user-desktop)" "/root/my-fancy-desktop" +assert_eq "$(systemd-path user-documents)" "/root/top/secret/documents" +assert_eq "$(systemd-path user-download)" "/root" +assert_eq "$(systemd-path user-music)" "/tmp/vaporwave" +assert_eq "$(systemd-path user-pictures)" "/root/Pictures" +assert_eq "$(systemd-path user-public)" "/root/cat-pictures" +assert_eq "$(systemd-path user-templates)" "/templates" +assert_eq "$(systemd-path user-videos)" "/root/🤔" + +# Remove the user-dirs.dir file and check the defaults +rm -fv "$USER_DIRS_CONF" +[[ ! -e "$USER_DIRS_CONF" ]] +assert_eq "$(systemd-path user-desktop)" "/root/Desktop" +for dir in "" documents download music pictures public templates videos; do + assert_eq "$(systemd-path "user${dir:+-$dir}")" "/root" +done + +# sd-path should consider only absolute $HOME +assert_eq "$(HOME=/hello-world systemd-path user)" "/hello-world" +assert_eq "$(HOME=hello-world systemd-path user)" "/root" +assert_eq "$(HOME=/hello systemd-path --suffix=world user)" "/hello/world" +assert_eq "$(HOME=hello systemd-path --suffix=world user)" "/root/world" +# Same with some other env variables +assert_in "/my-config" "$(HOME='' XDG_CONFIG_HOME=/my-config systemd-path search-configuration)" +assert_in "/my-config/foo" "$(HOME='' XDG_CONFIG_HOME=/my-config systemd-path --suffix=foo search-configuration)" +assert_in "/my-home/.config/foo" "$(HOME=/my-home XDG_CONFIG_HOME=my-config systemd-path --suffix=foo search-configuration)" +assert_not_in "my-config" "$(HOME=my-config XDG_CONFIG_HOME=my-config systemd-path search-configuration)" + +(! systemd-path '') +(! systemd-path system-binaries 🤔 user) +(! systemd-path --xyz) diff --git a/test/units/testsuite-74.pstore.sh b/test/units/testsuite-74.pstore.sh new file mode 100755 index 0000000..9be8066 --- /dev/null +++ b/test/units/testsuite-74.pstore.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemctl log-level info + +if systemd-detect-virt -cq; then + echo "Running in a container, skipping the systemd-pstore test..." + exit 0 +fi + +DUMMY_DMESG_0="$(mktemp)" +cat >"$DUMMY_DMESG_0" <<\EOF +6,17159,5340096332127,-;usb 1-4: USB disconnect, device number 124 +6,17160,5340109662397,-;input: WH-1000XM3 (AVRCP) as /devices/virtual/input/input293 +6,17161,5343126458360,-;loop0: detected capacity change from 0 to 3145728 +6,17162,5343126766065,-; loop0: p1 p2 +6,17163,5343126815038,-;EXT4-fs (loop0p1): mounted filesystem with ordered data mode. Quota mode: none. +6,17164,5343158037334,-;EXT4-fs (loop0p1): unmounting filesystem. +6,17165,5343158072598,-;loop0: detected capacity change from 0 to 3145728 +6,17166,5343158073563,-; loop0: p1 p2 +6,17167,5343158074325,-; loop0: p1 p2 +6,17168,5343158140859,-;EXT4-fs (loop0p1): mounted filesystem with ordered data mode. Quota mode: none. +6,17169,5343158182977,-;EXT4-fs (loop0p1): unmounting filesystem. +6,17170,5343158700241,-;loop0: detected capacity change from 0 to 3145728 +6,17171,5343158700439,-; loop0: p1 p2 +6,17172,5343158701120,-; loop0: p1 p2 +EOF + +DUMMY_DMESG_1="$(mktemp)" +cat >"$DUMMY_DMESG_1" <<\EOF +Nechť již hříšné saxofony ďáblů rozezvučí síň úděsnými tóny waltzu, tanga a quickstepu. +Příliš žluťoučký kůň úpěl ďábelské ódy. +Zvlášť zákeřný učeň s ďolíčky běží podél zóny úlů. +Vyciď křišťálový nůž, ó učiň úděsné líbivým! +Loď čeří kýlem tůň obzvlášť v Grónské úžině +Ó, náhlý déšť již zvířil prach a čilá laň teď běží s houfcem gazel k úkrytům. +Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk. +Kŕdeľ šťastných ďatľov učí pri ústí Váhu mĺkveho koňa obhrýzať kôru a žrať čerstvé mäso. +Stróż pchnął kość w quiz gędźb vel fax myjń. +Portez ce vieux whisky au juge blond qui fume! +EOF + +file_count() { find "${1:?}" -type f | wc -l; } +file_size() { wc -l <"${1:?}"; } +random_efi_timestamp() { printf "%0.10d" "$((1000000000 + RANDOM))"; } + +# The dmesg- filename contains the backend-type and the Common Platform Error Record, CPER, +# record id, a 64-bit number. +# +# Files are processed in reverse lexigraphical order so as to properly reconstruct original dmesg. + +prepare_efi_logs() { + local file="${1:?}" + local timestamp="${2:?}" + local chunk count filename + + # For the EFI backend, the 3 least significant digits of record id encodes a + # "count" number, the next 2 least significant digits for the dmesg part + # (chunk) number, and the remaining digits as the timestamp. See + # linux/drivers/firmware/efi/efi-pstore.c in efi_pstore_write(). + count="$(file_size "$file")" + chunk=0 + # The sed in the process substitution below just reverses the file + while read -r line; do + filename="$(printf "dmesg-efi-%0.10d%0.2d%0.3d" "$timestamp" "$chunk" "$count")" + echo "$line" >"/sys/fs/pstore/$filename" + chunk=$((chunk + 1)) + done < <(sed '1!G;h;$!d' "$file") + + if [[ "$chunk" -eq 0 ]]; then + echo >&2 "No dmesg-efi files were created" + exit 1 + fi +} + +prepare_erst_logs() { + local file="${1:?}" + local start_id="${2:?}" + local id filename + + # For the ERST backend, the record is a monotonically increasing number, seeded as + # a timestamp. See linux/drivers/acpi/apei/erst.c in erst_writer(). + id="$start_id" + # The sed in the process substitution below just reverses the file + while read -r line; do + filename="$(printf "dmesg-erst-%0.16d" "$id")" + echo "$line" >"/sys/fs/pstore/$filename" + id=$((id + 1)) + done < <(sed '1!G;h;$!d' "$file") + + if [[ "$id" -eq "$start_id" ]]; then + echo >&2 "No dmesg-erst files were created" + exit 1 + fi + + # ID of the last dmesg file will be the ID of the erst subfolder + echo "$((id - 1))" +} + +prepare_pstore_config() { + local storage="${1:?}" + local unlink="${2:?}" + + systemctl stop systemd-pstore + + rm -fr /sys/fs/pstore/* /var/lib/systemd/pstore/* + + mkdir -p /run/systemd/pstore.conf.d + cat >/run/systemd/pstore.conf.d/99-test.conf <<EOF +[PStore] +Storage=$storage +Unlink=$unlink +EOF + + systemd-analyze cat-config systemd/pstore.conf | grep "$storage" + systemd-analyze cat-config systemd/pstore.conf | grep "$unlink" +} + +start_pstore() { + rm -f /tmp/journal.cursor + journalctl -q -n 0 --cursor-file=/tmp/journal.cursor + systemctl restart systemd-pstore + journalctl --sync +} + +at_exit() { + set +e + + mountpoint -q /sys/fs/pstore && umount /sys/fs/pstore + rm -fr /var/lib/systemd/pstore/* + rm -f /run/systemd/system/systemd-pstore.service.d/99-StartLimitInterval.conf + rm -f /run/systemd/pstore.conf.d/99-test.conf +} + +trap at_exit EXIT + +# To avoid having to depend on the VM providing the pstore, let's simulate +# it using a simple bind mount +PSTORE_DIR="$(mktemp -d)" +mount --bind "${PSTORE_DIR:?}" "/sys/fs/pstore" + +# Disable the start limit since we're going to restart the systemd-pstore +# service quite a lot in a short time span +mkdir -p /run/systemd/system/systemd-pstore.service.d +cat >/run/systemd/system/systemd-pstore.service.d/99-StartLimitInterval.conf <<EOF +[Unit] +StartLimitInterval=0 +EOF +systemctl daemon-reload + +# systemd-pstore is a no-op with Storage=none +for unlink in yes no; do + : "Backend: N/A; Storage: none; Unlink: $unlink" + timestamp="$(random_efi_timestamp)" + prepare_pstore_config "none" "$unlink" + prepare_efi_logs "$DUMMY_DMESG_0" "$timestamp" + old_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$old_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -eq 0 ]] + + : "Backend: EFI; Storage: external; Unlink: $unlink" + timestamp="$(random_efi_timestamp)" + prepare_pstore_config "external" "$unlink" + prepare_efi_logs "$DUMMY_DMESG_0" "$timestamp" + [[ "$unlink" == yes ]] && exp_count=0 || exp_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$exp_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -ne 0 ]] + # We always log to journal + diff "$DUMMY_DMESG_0" <(journalctl -o cat --output-fields=FILE --cursor-file=/tmp/journal.cursor | sed "/^$/d") + filename="$(printf "/var/lib/systemd/pstore/%s/%0.3d/dmesg.txt" "$timestamp" "$(file_size "$DUMMY_DMESG_0")")" + diff "$DUMMY_DMESG_0" "$filename" + + : "Backend: EFI; Storage: external; Unlink: $unlink; multiple dmesg files" + timestamps=() + timestamp="$(random_efi_timestamp)" + prepare_pstore_config "external" "$unlink" + for i in {0..6}; do + timestamp="$((timestamp + (i * 10)))" + timestamps+=("$timestamp") + # Create a name reference to one of the $DUMMY_DMESG_X variables + dmesg="DUMMY_DMESG_$((i % 2))" + prepare_efi_logs "${!dmesg}" "$timestamp" + # Add one "random" (non-dmesg) file as well + echo "hello world" >/sys/fs/pstore/foo.bar + [[ "$unlink" == yes ]] && exp_count=0 || exp_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$exp_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -ne 0 ]] + filename="$(printf "/var/lib/systemd/pstore/%s/%0.3d/dmesg.txt" "$timestamp" "$(file_size "${!dmesg}")")" + diff "${!dmesg}" "$filename" + grep "hello world" "/var/lib/systemd/pstore/foo.bar" + done + # Check that we kept all previous records as well + for timestamp in "${timestamps[@]}"; do + [[ -d "/var/lib/systemd/pstore/$timestamp" ]] + [[ "$(file_count "/var/lib/systemd/pstore/$timestamp/")" -gt 0 ]] + done + + : "Backend: EFI; Storage: journal; Unlink: $unlink" + timestamp="$(random_efi_timestamp)" + prepare_pstore_config "journal" "$unlink" + prepare_efi_logs "$DUMMY_DMESG_0" "$timestamp" + [[ "$unlink" == yes ]] && exp_count=0 || exp_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$exp_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -eq 0 ]] + diff "$DUMMY_DMESG_0" <(journalctl -o cat --output-fields=FILE --cursor-file=/tmp/journal.cursor | sed "/^$/d") + + : "Backend: ERST; Storage: external; Unlink: $unlink" + prepare_pstore_config "external" "$unlink" + last_id="$(prepare_erst_logs "$DUMMY_DMESG_0" 0)" + [[ "$unlink" == yes ]] && exp_count=0 || exp_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$exp_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -ne 0 ]] + # We always log to journal + diff "$DUMMY_DMESG_0" <(journalctl -o cat --output-fields=FILE --cursor-file=/tmp/journal.cursor | sed "/^$/d") + filename="$(printf "/var/lib/systemd/pstore/%0.16d/dmesg.txt" "$last_id")" + diff "$DUMMY_DMESG_0" "$filename" + + : "Backend: ERST; Storage: external; Unlink: $unlink; multiple dmesg files" + last_ids=() + prepare_pstore_config "external" "$unlink" + for i in {0..9}; do + # Create a name reference to one of the $DUMMY_DMESG_X variables + dmesg="DUMMY_DMESG_$((i % 2))" + last_id="$(prepare_erst_logs "${!dmesg}" "$((i * 100))")" + last_ids+=("$last_id") + # Add one "random" (non-dmesg) file as well + echo "hello world" >/sys/fs/pstore/foo.bar + [[ "$unlink" == yes ]] && exp_count=0 || exp_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$exp_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -ne 0 ]] + filename="$(printf "/var/lib/systemd/pstore/%0.16d/dmesg.txt" "$last_id")" + diff "${!dmesg}" "$filename" + grep "hello world" "/var/lib/systemd/pstore/foo.bar" + done + # Check that we kept all previous records as well + for last_id in "${last_ids[@]}"; do + directory="$(printf "/var/lib/systemd/pstore/%0.16d" "$last_id")" + [[ -d "$directory" ]] + [[ "$(file_count "$directory")" -gt 0 ]] + done + + : "Backend: ERST; Storage: journal; Unlink: $unlink" + prepare_pstore_config "journal" "$unlink" + last_id="$(prepare_erst_logs "$DUMMY_DMESG_0" 0)" + [[ "$unlink" == yes ]] && exp_count=0 || exp_count="$(file_count /sys/fs/pstore/)" + start_pstore + [[ "$(file_count /sys/fs/pstore)" -ge "$exp_count" ]] + [[ "$(file_count /var/lib/systemd/pstore/)" -eq 0 ]] + diff "$DUMMY_DMESG_0" <(journalctl -o cat --output-fields=FILE --cursor-file=/tmp/journal.cursor | sed "/^$/d") +done diff --git a/test/units/testsuite-74.run.sh b/test/units/testsuite-74.run.sh new file mode 100755 index 0000000..e894932 --- /dev/null +++ b/test/units/testsuite-74.run.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +systemd-run --help --no-pager +systemd-run --version +systemd-run --no-ask-password true +systemd-run --no-block --collect true + +export PARENT_FOO=bar +touch /tmp/public-marker + +: "Transient service (system daemon)" +systemd-run --wait --pipe \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/run-.+\.service$ ]]' +systemd-run --wait --pipe --system \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/run-.+\.service$ ]]' +systemd-run --wait --pipe --slice=foo \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /foo\.slice/run-.+\.service$ ]]' +systemd-run --wait --pipe --slice=foo.slice \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /foo\.slice/run-.+\.service$ ]]' +systemd-run --wait --pipe --slice-inherit \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/run-.+\.service$ ]]' +systemd-run --wait --pipe --slice-inherit --slice=foo \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/system-foo\.slice/run-.+\.service$ ]]' +# We should not inherit caller's environment +systemd-run --wait --pipe bash -xec '[[ -z "$PARENT_FOO" ]]' +systemd-run --wait --pipe bash -xec '[[ "$PWD" == / && -n "$INVOCATION_ID" ]]' +systemd-run --wait --pipe \ + --send-sighup \ + --working-directory="" \ + --working-directory=/tmp \ + bash -xec '[[ "$PWD" == /tmp ]]' +systemd-run --wait --pipe --same-dir bash -xec "[[ \"\$PWD\" == $PWD ]]" +systemd-run --wait --pipe \ + --property=LimitCORE=1M:2M \ + --property=LimitCORE=16M:32M \ + --property=PrivateTmp=yes \ + bash -xec '[[ "$(ulimit -c -S)" -eq 16384 && "$(ulimit -c -H)" -eq 32768 && ! -e /tmp/public-marker ]]' +systemd-run --wait --pipe \ + --uid=testuser \ + bash -xec '[[ "$(id -nu)" == testuser && "$(id -ng)" == testuser ]]' +systemd-run --wait --pipe \ + --gid=testuser \ + bash -xec '[[ "$(id -nu)" == root && "$(id -ng)" == testuser ]]' +systemd-run --wait --pipe \ + --uid=testuser \ + --gid=root \ + bash -xec '[[ "$(id -nu)" == testuser && "$(id -ng)" == root ]]' +systemd-run --wait --pipe --expand-environment=no \ + --nice=10 \ + bash -xec 'read -r -a SELF_STAT </proc/self/stat && [[ "${SELF_STAT[18]}" -eq 10 ]]' +systemd-run --wait --pipe \ + --setenv=ENV_HELLO="nope" \ + --setenv=ENV_HELLO="env world" \ + --setenv=EMPTY= \ + --setenv=PARENT_FOO \ + --property=Environment="ALSO_HELLO='also world'" \ + bash -xec '[[ "$ENV_HELLO" == "env world" && -z "$EMPTY" && "$PARENT_FOO" == bar && "$ALSO_HELLO" == "also world" ]]' + +UNIT="service-0-$RANDOM" +systemd-run --remain-after-exit --unit="$UNIT" \ + --service-type=simple \ + --service-type=oneshot \ + true +systemctl cat "$UNIT" +grep -q "^Type=oneshot" "/run/systemd/transient/$UNIT.service" +systemctl stop "$UNIT" +(! systemctl cat "$UNIT") + +: "Transient service (user daemon)" +systemd-run --wait --pipe --user --machine=testuser@ \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /user\.slice/.+/run-.+\.service$ ]]' +systemd-run --wait --pipe --user --machine=testuser@ \ + bash -xec '[[ "$(id -nu)" == testuser && "$(id -ng)" == testuser ]]' +systemd-run --wait --pipe --user --machine=testuser@ \ + bash -xec '[[ "$PWD" == /home/testuser && -n "$INVOCATION_ID" ]]' + +# PrivateTmp=yes implies PrivateUsers=yes for user manager, so skip this if we +# don't have unprivileged user namespaces. +if [[ "$(sysctl -ne kernel.apparmor_restrict_unprivileged_userns)" -ne 1 ]]; then + systemd-run --wait --pipe --user --machine=testuser@ \ + --property=LimitCORE=1M:2M \ + --property=LimitCORE=16M:32M \ + --property=PrivateTmp=yes \ + bash -xec '[[ "$(ulimit -c -S)" -eq 16384 && "$(ulimit -c -H)" -eq 32768 && ! -e /tmp/public-marker ]]' +fi + +: "Transient scope (system daemon)" +systemd-run --scope \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/run-.+\.scope$ ]]' +systemd-run --scope --system \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/run-.+\.scope$ ]]' +systemd-run --scope --slice=foo \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /foo\.slice/run-.+\.scope$ ]]' +systemd-run --scope --slice=foo.slice \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /foo\.slice/run-.+\.scope$ ]]' +systemd-run --scope --slice-inherit \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/run-.+\.scope$ ]]' +systemd-run --scope --slice-inherit --slice=foo \ + bash -xec '[[ "$(</proc/self/cgroup)" =~ /system\.slice/system-foo\.slice/run-.+\.scope$ ]]' +# We should inherit caller's environment +systemd-run --scope bash -xec '[[ "$PARENT_FOO" == bar ]]' +systemd-run --scope \ + --property=RuntimeMaxSec=10 \ + --property=RuntimeMaxSec=infinity \ + true + +: "Transient scope (user daemon)" +# FIXME: https://github.com/systemd/systemd/issues/27883 +#systemd-run --scope --user --machine=testuser@ \ +# bash -xec '[[ "$(</proc/self/cgroup)" =~ /user\.slice/run-.+\.scope$ ]]' +# We should inherit caller's environment +#systemd-run --scope --user --machine=testuser@ bash -xec '[[ "$PARENT_FOO" == bar ]]' + +: "Transient timer unit" +UNIT="timer-0-$RANDOM" +systemd-run --remain-after-exit \ + --unit="$UNIT" \ + --timer-property=OnUnitInactiveSec=16h \ + true +systemctl cat "$UNIT.service" "$UNIT.timer" +grep -q "^OnUnitInactiveSec=16h$" "/run/systemd/transient/$UNIT.timer" +grep -qE "^ExecStart=.*/bin/true.*$" "/run/systemd/transient/$UNIT.service" +systemctl stop "$UNIT.timer" "$UNIT.service" || : + +UNIT="timer-1-$RANDOM" +systemd-run --remain-after-exit \ + --unit="$UNIT" \ + --on-active=10 \ + --on-active=30s \ + --on-boot=1s \ + --on-startup=2m \ + --on-unit-active=3h20m \ + --on-unit-inactive="5d 4m 32s" \ + --on-calendar="mon,fri *-1/2-1,3 *:30:45" \ + --on-clock-change \ + --on-clock-change \ + --on-timezone-change \ + --timer-property=After=systemd-journald.service \ + --description="Hello world" \ + --description="My Fancy Timer" \ + true +systemctl cat "$UNIT.service" "$UNIT.timer" +systemd-analyze verify --recursive-errors=no "/run/systemd/transient/$UNIT.service" +systemd-analyze verify --recursive-errors=no "/run/systemd/transient/$UNIT.timer" +grep -q "^Description=My Fancy Timer$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnActiveSec=10s$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnActiveSec=30s$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnBootSec=1s$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnStartupSec=2min$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnUnitActiveSec=3h 20min$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnUnitInactiveSec=5d 4min 32s$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnCalendar=mon,fri \*\-1/2\-1,3 \*:30:45$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnClockChange=yes$" "/run/systemd/transient/$UNIT.timer" +grep -q "^OnTimezoneChange=yes$" "/run/systemd/transient/$UNIT.timer" +grep -q "^After=systemd-journald.service$" "/run/systemd/transient/$UNIT.timer" +grep -q "^Description=My Fancy Timer$" "/run/systemd/transient/$UNIT.service" +grep -q "^RemainAfterExit=yes$" "/run/systemd/transient/$UNIT.service" +grep -qE "^ExecStart=.*/bin/true.*$" "/run/systemd/transient/$UNIT.service" +(! grep -q "^After=systemd-journald.service$" "/run/systemd/transient/$UNIT.service") +systemctl stop "$UNIT.timer" "$UNIT.service" || : + +: "Transient path unit" +UNIT="path-0-$RANDOM" +systemd-run --remain-after-exit \ + --unit="$UNIT" \ + --path-property=PathExists=/tmp \ + --path-property=PathExists=/tmp/foo \ + --path-property=PathChanged=/root/bar \ + true +systemctl cat "$UNIT.service" "$UNIT.path" +systemd-analyze verify --recursive-errors=no "/run/systemd/transient/$UNIT.service" +systemd-analyze verify --recursive-errors=no "/run/systemd/transient/$UNIT.path" +grep -q "^PathExists=/tmp$" "/run/systemd/transient/$UNIT.path" +grep -q "^PathExists=/tmp/foo$" "/run/systemd/transient/$UNIT.path" +grep -q "^PathChanged=/root/bar$" "/run/systemd/transient/$UNIT.path" +grep -qE "^ExecStart=.*/bin/true.*$" "/run/systemd/transient/$UNIT.service" +systemctl stop "$UNIT.path" "$UNIT.service" || : + +: "Transient socket unit" +UNIT="socket-0-$RANDOM" +systemd-run --remain-after-exit \ + --unit="$UNIT" \ + --socket-property=ListenFIFO=/tmp/socket.fifo \ + --socket-property=SocketMode=0666 \ + --socket-property=SocketMode=0644 \ + true +systemctl cat "$UNIT.service" "$UNIT.socket" +systemd-analyze verify --recursive-errors=no "/run/systemd/transient/$UNIT.service" +systemd-analyze verify --recursive-errors=no "/run/systemd/transient/$UNIT.socket" +grep -q "^ListenFIFO=/tmp/socket.fifo$" "/run/systemd/transient/$UNIT.socket" +grep -q "^SocketMode=0666$" "/run/systemd/transient/$UNIT.socket" +grep -q "^SocketMode=0644$" "/run/systemd/transient/$UNIT.socket" +grep -qE "^ExecStart=.*/bin/true.*$" "/run/systemd/transient/$UNIT.service" +systemctl stop "$UNIT.socket" "$UNIT.service" || : + +: "Interactive options" +SHELL=/bin/true systemd-run --shell +SHELL=/bin/true systemd-run --scope --shell +systemd-run --wait --pty true +systemd-run --wait --machine=.host --pty true +(! SHELL=/bin/false systemd-run --quiet --shell) + +(! systemd-run) +(! systemd-run "") +(! systemd-run --foo=bar) +(! systemd-run --wait --pipe --slice=foo.service true) + +for opt in nice on-{active,boot,calendar,startup,unit-active,unit-inactive} property service-type setenv; do + (! systemd-run "--$opt=" true) + (! systemd-run "--$opt=''" true) +done + +# Let's make sure that ProtectProc= properly moves submounts of the original /proc over to the new proc + +A=$(cat /proc/sys/kernel/random/boot_id) +B=$(systemd-run -q --wait --pipe -p ProtectProc=invisible cat /proc/sys/kernel/random/boot_id) +assert_eq "$A" "$B" + +V="/tmp/version.$RANDOM" +A="$(cat /proc/version).piff" +echo "$A" > "$V" +mount --bind "$V" /proc/version + +B=$(systemd-run -q --wait --pipe -p ProtectProc=invisible cat /proc/version) + +assert_eq "$A" "$B" + +umount /proc/version +rm "$V" diff --git a/test/units/testsuite-74.service b/test/units/testsuite-74.service new file mode 100644 index 0000000..f782132 --- /dev/null +++ b/test/units/testsuite-74.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-74-AUX-UTILS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-74.sh b/test/units/testsuite-74.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-74.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-74.varlinkctl.sh b/test/units/testsuite-74.varlinkctl.sh new file mode 100755 index 0000000..5a96269 --- /dev/null +++ b/test/units/testsuite-74.varlinkctl.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Unset $PAGER so we don't have to use --no-pager everywhere +export PAGER= + +varlinkctl --help +varlinkctl help --no-pager +varlinkctl --version +varlinkctl --json=help + +# TODO: abstract namespace sockets (@...) +# Path to a socket +varlinkctl info /run/systemd/journal/io.systemd.journal +varlinkctl info /run/systemd/../systemd/../../run/systemd/journal/io.systemd.journal +varlinkctl info "./$(realpath --relative-to="$PWD" /run/systemd/journal/io.systemd.journal)" +varlinkctl info unix:/run/systemd/journal/io.systemd.journal +varlinkctl info --json=off /run/systemd/journal/io.systemd.journal +varlinkctl info --json=pretty /run/systemd/journal/io.systemd.journal | jq . +varlinkctl info --json=short /run/systemd/journal/io.systemd.journal | jq . +varlinkctl info -j /run/systemd/journal/io.systemd.journal | jq . + +varlinkctl list-interfaces /run/systemd/journal/io.systemd.journal +varlinkctl list-interfaces -j /run/systemd/journal/io.systemd.journal | jq . + +varlinkctl introspect /run/systemd/journal/io.systemd.journal io.systemd.Journal +varlinkctl introspect -j /run/systemd/journal/io.systemd.journal io.systemd.Journal | jq . + +if command -v userdbctl >/dev/null; then + systemctl start systemd-userdbd + varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{ "userName" : "testuser", "service" : "io.systemd.Multiplexer" }' + varlinkctl call -j /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{ "userName" : "testuser", "service" : "io.systemd.Multiplexer" }' | jq . + varlinkctl call --more /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetMemberships '{ "service" : "io.systemd.Multiplexer" }' + varlinkctl call --more -j /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetMemberships '{ "service" : "io.systemd.Multiplexer" }' | jq --seq . + varlinkctl call --oneway /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetMemberships '{ "service" : "io.systemd.Multiplexer" }' + (! varlinkctl call --oneway /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetMemberships '{ "service" : "io.systemd.Multiplexer" }' | grep .) +fi + +IDL_FILE="$(mktemp)" +varlinkctl introspect /run/systemd/journal/io.systemd.journal io.systemd.Journal | tee "${IDL_FILE:?}" +varlinkctl validate-idl "$IDL_FILE" +varlinkctl validate-idl "$IDL_FILE" +cat /bin/sh >"$IDL_FILE" +(! varlinkctl validate-idl "$IDL_FILE") + +if [[ -x /usr/lib/systemd/systemd-pcrextend ]]; then + # Path to an executable + varlinkctl info /usr/lib/systemd/systemd-pcrextend + varlinkctl info exec:/usr/lib/systemd/systemd-pcrextend + varlinkctl list-interfaces /usr/lib/systemd/systemd-pcrextend + varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend +fi + +# Go through all varlink sockets we can find under /run/systemd/ for some extra coverage +find /run/systemd/ -name "io.systemd*" -type s | while read -r socket; do + varlinkctl info "$socket" + + varlinkctl list-interfaces "$socket" | while read -r interface; do + varlinkctl introspect "$socket" "$interface" + done +done + +(! varlinkctl) +(! varlinkctl "") +(! varlinkctl info) +(! varlinkctl info "") +(! varlinkctl info /run/systemd/notify) +(! varlinkctl info /run/systemd/private) +# Relative paths must begin with ./ +(! varlinkctl info "$(realpath --relative-to="$PWD" /run/systemd/journal/io.systemd.journal)") +(! varlinkctl info unix:) +(! varlinkctl info unix:"") +(! varlinkctl info exec:) +(! varlinkctl info exec:"") +(! varlinkctl list-interfaces) +(! varlinkctl list-interfaces "") +(! varlinkctl introspect) +(! varlinkctl introspect /run/systemd/journal/io.systemd.journal) +(! varlinkctl introspect /run/systemd/journal/io.systemd.journal "") +(! varlinkctl introspect "" "") +(! varlinkctl call) +(! varlinkctl call "") +(! varlinkctl call "" "") +(! varlinkctl call "" "" "") +(! varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord </dev/null) +(! varlinkctl validate-idl "") +(! varlinkctl validate-idl </dev/null) diff --git a/test/units/testsuite-75.service b/test/units/testsuite-75.service new file mode 100644 index 0000000..111cde3 --- /dev/null +++ b/test/units/testsuite-75.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Tests for systemd-resolved + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-75.sh b/test/units/testsuite-75.sh new file mode 100755 index 0000000..5423448 --- /dev/null +++ b/test/units/testsuite-75.sh @@ -0,0 +1,729 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# vi: ts=4 sw=4 tw=0 et: + +# TODO: +# - IPv6-only stack +# - mDNS +# - LLMNR +# - DoT/DoH + +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +RUN_OUT="$(mktemp)" + +run() { + "$@" |& tee "$RUN_OUT" +} + +run_delv() { + # Since [0] delv no longer loads /etc/(bind/)bind.keys by default, so we + # have to do that explicitly for each invocation + run delv -a /etc/bind.keys "$@" +} + +disable_ipv6() { + sysctl -w net.ipv6.conf.all.disable_ipv6=1 +} + +enable_ipv6() { + sysctl -w net.ipv6.conf.all.disable_ipv6=0 + networkctl reconfigure dns0 + /usr/lib/systemd/systemd-networkd-wait-online --ipv4 --ipv6 --interface=dns0:routable --timeout=30 +} + +monitor_check_rr() ( + set +x + set +o pipefail + local since="${1:?}" + local match="${2:?}" + + # Wait until the first mention of the specified log message is + # displayed. We turn off pipefail for this, since we don't care about the + # lhs of this pipe expression, we only care about the rhs' result to be + # clean + timeout -v 30s journalctl -u resolvectl-monitor.service --since "$since" -f --full | grep -m1 "$match" +) + +restart_resolved() { + systemctl stop systemd-resolved.service + (! systemctl is-failed systemd-resolved.service) + # Reset the restart counter since we call this method a bunch of times + # and can occasionally hit the default rate limit + systemctl reset-failed systemd-resolved.service + systemctl start systemd-resolved.service + systemctl service-log-level systemd-resolved.service debug +} + +# Test for resolvectl, resolvconf +systemctl unmask systemd-resolved.service +systemctl enable --now systemd-resolved.service +systemctl service-log-level systemd-resolved.service debug +ip link add hoge type dummy +ip link add hoge.foo type dummy +resolvectl dns hoge 10.0.0.1 10.0.0.2 +resolvectl dns hoge.foo 10.0.0.3 10.0.0.4 +assert_in '10.0.0.1 10.0.0.2' "$(resolvectl dns hoge)" +assert_in '10.0.0.3 10.0.0.4' "$(resolvectl dns hoge.foo)" +resolvectl dns hoge 10.0.1.1 10.0.1.2 +resolvectl dns hoge.foo 10.0.1.3 10.0.1.4 +assert_in '10.0.1.1 10.0.1.2' "$(resolvectl dns hoge)" +assert_in '10.0.1.3 10.0.1.4' "$(resolvectl dns hoge.foo)" +if ! RESOLVCONF=$(command -v resolvconf 2>/dev/null); then + TMPDIR=$(mktemp -d -p /tmp resolvconf-tests.XXXXXX) + RESOLVCONF="$TMPDIR"/resolvconf + ln -s "$(command -v resolvectl 2>/dev/null)" "$RESOLVCONF" +fi +echo nameserver 10.0.2.1 10.0.2.2 | "$RESOLVCONF" -a hoge +echo nameserver 10.0.2.3 10.0.2.4 | "$RESOLVCONF" -a hoge.foo +assert_in '10.0.2.1 10.0.2.2' "$(resolvectl dns hoge)" +assert_in '10.0.2.3 10.0.2.4' "$(resolvectl dns hoge.foo)" +echo nameserver 10.0.3.1 10.0.3.2 | "$RESOLVCONF" -a hoge.inet.ipsec.192.168.35 +echo nameserver 10.0.3.3 10.0.3.4 | "$RESOLVCONF" -a hoge.foo.dhcp +assert_in '10.0.3.1 10.0.3.2' "$(resolvectl dns hoge)" +assert_in '10.0.3.3 10.0.3.4' "$(resolvectl dns hoge.foo)" + +# Tests for _localdnsstub and _localdnsproxy +assert_in '127.0.0.53' "$(resolvectl query _localdnsstub)" +assert_in '_localdnsstub' "$(resolvectl query 127.0.0.53)" +assert_in '127.0.0.54' "$(resolvectl query _localdnsproxy)" +assert_in '_localdnsproxy' "$(resolvectl query 127.0.0.54)" + +assert_in '127.0.0.53' "$(dig @127.0.0.53 _localdnsstub)" +assert_in '_localdnsstub' "$(dig @127.0.0.53 -x 127.0.0.53)" +assert_in '127.0.0.54' "$(dig @127.0.0.53 _localdnsproxy)" +assert_in '_localdnsproxy' "$(dig @127.0.0.53 -x 127.0.0.54)" + +# Tests for mDNS and LLMNR settings +mkdir -p /run/systemd/resolved.conf.d +{ + echo "[Resolve]" + echo "MulticastDNS=yes" + echo "LLMNR=yes" +} >/run/systemd/resolved.conf.d/mdns-llmnr.conf +restart_resolved +# make sure networkd is not running. +systemctl stop systemd-networkd.service +# defaults to yes (both the global and per-link settings are yes) +assert_in 'yes' "$(resolvectl mdns hoge)" +assert_in 'yes' "$(resolvectl llmnr hoge)" +# set per-link setting +resolvectl mdns hoge yes +resolvectl llmnr hoge yes +assert_in 'yes' "$(resolvectl mdns hoge)" +assert_in 'yes' "$(resolvectl llmnr hoge)" +resolvectl mdns hoge resolve +resolvectl llmnr hoge resolve +assert_in 'resolve' "$(resolvectl mdns hoge)" +assert_in 'resolve' "$(resolvectl llmnr hoge)" +resolvectl mdns hoge no +resolvectl llmnr hoge no +assert_in 'no' "$(resolvectl mdns hoge)" +assert_in 'no' "$(resolvectl llmnr hoge)" +# downgrade global setting to resolve +{ + echo "[Resolve]" + echo "MulticastDNS=resolve" + echo "LLMNR=resolve" +} >/run/systemd/resolved.conf.d/mdns-llmnr.conf +restart_resolved +# set per-link setting +resolvectl mdns hoge yes +resolvectl llmnr hoge yes +assert_in 'resolve' "$(resolvectl mdns hoge)" +assert_in 'resolve' "$(resolvectl llmnr hoge)" +resolvectl mdns hoge resolve +resolvectl llmnr hoge resolve +assert_in 'resolve' "$(resolvectl mdns hoge)" +assert_in 'resolve' "$(resolvectl llmnr hoge)" +resolvectl mdns hoge no +resolvectl llmnr hoge no +assert_in 'no' "$(resolvectl mdns hoge)" +assert_in 'no' "$(resolvectl llmnr hoge)" +# downgrade global setting to no +{ + echo "[Resolve]" + echo "MulticastDNS=no" + echo "LLMNR=no" +} >/run/systemd/resolved.conf.d/mdns-llmnr.conf +restart_resolved +# set per-link setting +resolvectl mdns hoge yes +resolvectl llmnr hoge yes +assert_in 'no' "$(resolvectl mdns hoge)" +assert_in 'no' "$(resolvectl llmnr hoge)" +resolvectl mdns hoge resolve +resolvectl llmnr hoge resolve +assert_in 'no' "$(resolvectl mdns hoge)" +assert_in 'no' "$(resolvectl llmnr hoge)" +resolvectl mdns hoge no +resolvectl llmnr hoge no +assert_in 'no' "$(resolvectl mdns hoge)" +assert_in 'no' "$(resolvectl llmnr hoge)" + +# Cleanup +rm -f /run/systemd/resolved.conf.d/mdns-llmnr.conf +ip link del hoge +ip link del hoge.foo + +### SETUP ### +# Configure network +hostnamectl hostname ns1.unsigned.test +cat >>/etc/hosts <<EOF +10.0.0.1 ns1.unsigned.test +fd00:dead:beef:cafe::1 ns1.unsigned.test + +127.128.0.5 localhost5 localhost5.localdomain localhost5.localdomain4 localhost.localdomain5 localhost5.localdomain5 +EOF + +mkdir -p /etc/systemd/network +cat >/etc/systemd/network/10-dns0.netdev <<EOF +[NetDev] +Name=dns0 +Kind=dummy +EOF +cat >/etc/systemd/network/10-dns0.network <<EOF +[Match] +Name=dns0 + +[Network] +Address=10.0.0.1/24 +Address=fd00:dead:beef:cafe::1/64 +DNSSEC=allow-downgrade +DNS=10.0.0.1 +DNS=fd00:dead:beef:cafe::1 +EOF + +DNS_ADDRESSES=( + "10.0.0.1" + "fd00:dead:beef:cafe::1" +) + +mkdir -p /run/systemd/resolved.conf.d +{ + echo "[Resolve]" + echo "FallbackDNS=" + echo "DNSSEC=allow-downgrade" + echo "DNSOverTLS=opportunistic" +} >/run/systemd/resolved.conf.d/test.conf +ln -svf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf +# Override the default NTA list, which turns off DNSSEC validation for (among +# others) the test. domain +mkdir -p "/etc/dnssec-trust-anchors.d/" +echo local >/etc/dnssec-trust-anchors.d/local.negative + +# Sign the root zone +keymgr . generate algorithm=ECDSAP256SHA256 ksk=yes zsk=yes +# Create a trust anchor for resolved with our root zone +keymgr . ds | sed 's/ DS/ IN DS/g' >/etc/dnssec-trust-anchors.d/root.positive +# Create a bind-compatible trust anchor (for delv) +# Note: the trust-anchors directive is relatively new, so use the original +# managed-keys one until it's widespread enough +{ + echo 'managed-keys {' + keymgr . dnskey | sed -r 's/^\. DNSKEY ([0-9]+ [0-9]+ [0-9]+) (.+)$/. static-key \1 "\2";/g' + echo '};' +} >/etc/bind.keys +# Create an /etc/bind/bind.keys symlink, which is used by delv on Ubuntu +mkdir -p /etc/bind +ln -svf /etc/bind.keys /etc/bind/bind.keys + +# Start the services +systemctl unmask systemd-networkd +systemctl start systemd-networkd +restart_resolved +# Create knot's runtime dir, since from certain version it's provided only by +# the package and not created by tmpfiles/systemd +if [[ ! -d /run/knot ]]; then + mkdir -p /run/knot + chown -R knot:knot /run/knot +fi +systemctl start knot +# Wait a bit for the keys to propagate +sleep 4 + +networkctl status +resolvectl status +resolvectl log-level debug + +# Start monitoring queries +systemd-run -u resolvectl-monitor.service -p Type=notify resolvectl monitor +systemd-run -u resolvectl-monitor-json.service -p Type=notify resolvectl monitor --json=short + +# Check if all the zones are valid (zone-check always returns 0, so let's check +# if it produces any errors/warnings) +run knotc zone-check +[[ ! -s "$RUN_OUT" ]] +# We need to manually propagate the DS records of onlinesign.test. to the parent +# zone, since they're generated online +knotc zone-begin test. +if knotc zone-get test. onlinesign.test. ds | grep .; then + # Drop any old DS records, if present (e.g. on test re-run) + knotc zone-unset test. onlinesign.test. ds +fi +# Propagate the new DS records +while read -ra line; do + knotc zone-set test. "${line[0]}" 600 "${line[@]:1}" +done < <(keymgr onlinesign.test. ds) +knotc zone-commit test. + +knotc reload + +### SETUP END ### + +: "--- nss-resolve/nss-myhostname tests" +# Sanity check +TIMESTAMP=$(date '+%F %T') +# Issue: https://github.com/systemd/systemd/issues/23951 +# With IPv6 enabled +run getent -s resolve hosts ns1.unsigned.test +grep -qE "^fd00:dead:beef:cafe::1\s+ns1\.unsigned\.test" "$RUN_OUT" +monitor_check_rr "$TIMESTAMP" "ns1.unsigned.test IN AAAA fd00:dead:beef:cafe::1" +# With IPv6 disabled +# Issue: https://github.com/systemd/systemd/issues/23951 +# FIXME +#disable_ipv6 +#run getent -s resolve hosts ns1.unsigned.test +#grep -qE "^10\.0\.0\.1\s+ns1\.unsigned\.test" "$RUN_OUT" +#monitor_check_rr "$TIMESTAMP" "ns1.unsigned.test IN A 10.0.0.1" +enable_ipv6 + +# Issue: https://github.com/systemd/systemd/issues/18812 +# PR: https://github.com/systemd/systemd/pull/18896 +# Follow-up issue: https://github.com/systemd/systemd/issues/23152 +# Follow-up PR: https://github.com/systemd/systemd/pull/23161 +# With IPv6 enabled +run getent -s resolve hosts localhost +grep -qE "^::1\s+localhost" "$RUN_OUT" +run getent -s myhostname hosts localhost +grep -qE "^::1\s+localhost" "$RUN_OUT" +# With IPv6 disabled +disable_ipv6 +run getent -s resolve hosts localhost +grep -qE "^127\.0\.0\.1\s+localhost" "$RUN_OUT" +run getent -s myhostname hosts localhost +grep -qE "^127\.0\.0\.1\s+localhost" "$RUN_OUT" +enable_ipv6 + +# Issue: https://github.com/systemd/systemd/issues/25088 +run getent -s resolve hosts 127.128.0.5 +grep -qEx '127\.128\.0\.5\s+localhost5(\s+localhost5?\.localdomain[45]?){4}' "$RUN_OUT" +[ "$(wc -l <"$RUN_OUT")" -eq 1 ] + +# Issue: https://github.com/systemd/systemd/issues/20158 +run dig +noall +answer +additional localhost5. +grep -qEx 'localhost5\.\s+0\s+IN\s+A\s+127\.128\.0\.5' "$RUN_OUT" +[ "$(wc -l <"$RUN_OUT")" -eq 1 ] +run dig +noall +answer +additional localhost5.localdomain4. +grep -qEx 'localhost5\.localdomain4\.\s+0\s+IN\s+CNAME\s+localhost5\.' "$RUN_OUT" +grep -qEx 'localhost5\.\s+0\s+IN\s+A\s+127\.128\.0\.5' "$RUN_OUT" +[ "$(wc -l <"$RUN_OUT")" -eq 2 ] + +: "--- Basic resolved tests ---" +# Issue: https://github.com/systemd/systemd/issues/22229 +# PR: https://github.com/systemd/systemd/pull/22231 +FILTERED_NAMES=( + "0.in-addr.arpa" + "255.255.255.255.in-addr.arpa" + "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" + "hello.invalid" + "hello.alt" +) + +for name in "${FILTERED_NAMES[@]}"; do + (! run host "$name") + grep -qF "NXDOMAIN" "$RUN_OUT" +done + +# Follow-up +# Issue: https://github.com/systemd/systemd/issues/22401 +# PR: https://github.com/systemd/systemd/pull/22414 +run dig +noall +authority +comments SRV . +grep -qF "status: NOERROR" "$RUN_OUT" +grep -qE "IN\s+SOA\s+ns1\.unsigned\.test\." "$RUN_OUT" + + +: "--- ZONE: unsigned.test. ---" +run dig @ns1.unsigned.test +short unsigned.test A unsigned.test AAAA +grep -qF "10.0.0.101" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::101" "$RUN_OUT" +run resolvectl query unsigned.test +grep -qF "10.0.0.10" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::101" "$RUN_OUT" +grep -qF "authenticated: no" "$RUN_OUT" +run dig @ns1.unsigned.test +short MX unsigned.test +grep -qF "15 mail.unsigned.test." "$RUN_OUT" +run resolvectl query --legend=no -t MX unsigned.test +grep -qF "unsigned.test IN MX 15 mail.unsigned.test" "$RUN_OUT" + + +: "--- ZONE: signed.test (static DNSSEC) ---" +# Check the trust chain (with and without systemd-resolved in between +# Issue: https://github.com/systemd/systemd/issues/22002 +# PR: https://github.com/systemd/systemd/pull/23289 +run_delv @ns1.unsigned.test signed.test +grep -qF "; fully validated" "$RUN_OUT" +run_delv signed.test +grep -qF "; fully validated" "$RUN_OUT" + +for addr in "${DNS_ADDRESSES[@]}"; do + run_delv "@$addr" -t A mail.signed.test + grep -qF "; fully validated" "$RUN_OUT" + run_delv "@$addr" -t AAAA mail.signed.test + grep -qF "; fully validated" "$RUN_OUT" +done +run resolvectl query mail.signed.test +grep -qF "10.0.0.11" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::11" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" + +run dig +short signed.test +grep -qF "10.0.0.10" "$RUN_OUT" +run resolvectl query signed.test +grep -qF "signed.test: 10.0.0.10" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" +run dig @ns1.unsigned.test +short MX signed.test +grep -qF "10 mail.signed.test." "$RUN_OUT" +run resolvectl query --legend=no -t MX signed.test +grep -qF "signed.test IN MX 10 mail.signed.test" "$RUN_OUT" +# Check a non-existent domain +run dig +dnssec this.does.not.exist.signed.test +grep -qF "status: NXDOMAIN" "$RUN_OUT" +# Check a wildcard record +run resolvectl query -t TXT this.should.be.authenticated.wild.signed.test +grep -qF 'this.should.be.authenticated.wild.signed.test IN TXT "this is a wildcard"' "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" +# Check SRV support +run resolvectl service _mysvc._tcp signed.test +grep -qF "myservice.signed.test:1234" "$RUN_OUT" +grep -qF "10.0.0.20" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::17" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" +(! run resolvectl service _invalidsvc._udp signed.test) +grep -qE "invalidservice\.signed\.test' not found" "$RUN_OUT" +run resolvectl service _untrustedsvc._udp signed.test +grep -qF "myservice.untrusted.test:1111" "$RUN_OUT" +grep -qF "10.0.0.123" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::123" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" +# Check OPENPGPKEY support +run_delv -t OPENPGPKEY 5a786cdc59c161cdafd818143705026636962198c66ed4c5b3da321e._openpgpkey.signed.test +grep -qF "; fully validated" "$RUN_OUT" +run resolvectl openpgp mr.smith@signed.test +grep -qF "5a786cdc59c161cdafd818143705026636962198c66ed4c5b3da321e._openpgpkey.signed.test" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" + +# DNSSEC validation with multiple records of the same type for the same name +# Issue: https://github.com/systemd/systemd/issues/22002 +# PR: https://github.com/systemd/systemd/pull/23289 +check_domain() { + local domain="${1:?}" + local record="${2:?}" + local message="${3:?}" + local addr + + for addr in "${DNS_ADDRESSES[@]}"; do + run_delv "@$addr" -t "$record" "$domain" + grep -qF "$message" "$RUN_OUT" + done + + run_delv -t "$record" "$domain" + grep -qF "$message" "$RUN_OUT" + + run resolvectl query "$domain" + grep -qF "authenticated: yes" "$RUN_OUT" +} + +check_domain "dupe.signed.test" "A" "; fully validated" +check_domain "dupe.signed.test" "AAAA" "; negative response, fully validated" +check_domain "dupe-ipv6.signed.test" "AAAA" "; fully validated" +check_domain "dupe-ipv6.signed.test" "A" "; negative response, fully validated" +check_domain "dupe-mixed.signed.test" "A" "; fully validated" +check_domain "dupe-mixed.signed.test" "AAAA" "; fully validated" + +# Test resolution of CNAME chains +TIMESTAMP=$(date '+%F %T') +run resolvectl query -t A cname-chain.signed.test +grep -qF "follow14.final.signed.test IN A 10.0.0.14" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" + +monitor_check_rr "$TIMESTAMP" "follow10.so.close.signed.test IN CNAME follow11.yet.so.far.signed.test" +monitor_check_rr "$TIMESTAMP" "follow11.yet.so.far.signed.test IN CNAME follow12.getting.hot.signed.test" +monitor_check_rr "$TIMESTAMP" "follow12.getting.hot.signed.test IN CNAME follow13.almost.final.signed.test" +monitor_check_rr "$TIMESTAMP" "follow13.almost.final.signed.test IN CNAME follow14.final.signed.test" +monitor_check_rr "$TIMESTAMP" "follow14.final.signed.test IN A 10.0.0.14" + +# Non-existing RR + CNAME chain +run dig +dnssec AAAA cname-chain.signed.test +grep -qF "status: NOERROR" "$RUN_OUT" +grep -qE "^follow14\.final\.signed\.test\..+IN\s+NSEC\s+" "$RUN_OUT" + + +: "--- ZONE: onlinesign.test (dynamic DNSSEC) ---" +# Check the trust chain (with and without systemd-resolved in between +# Issue: https://github.com/systemd/systemd/issues/22002 +# PR: https://github.com/systemd/systemd/pull/23289 +run_delv @ns1.unsigned.test sub.onlinesign.test +grep -qF "; fully validated" "$RUN_OUT" +run_delv sub.onlinesign.test +grep -qF "; fully validated" "$RUN_OUT" + +run dig +short sub.onlinesign.test +grep -qF "10.0.0.133" "$RUN_OUT" +run resolvectl query sub.onlinesign.test +grep -qF "sub.onlinesign.test: 10.0.0.133" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" +run dig @ns1.unsigned.test +short TXT onlinesign.test +grep -qF '"hello from onlinesign"' "$RUN_OUT" +run resolvectl query --legend=no -t TXT onlinesign.test +grep -qF 'onlinesign.test IN TXT "hello from onlinesign"' "$RUN_OUT" + +for addr in "${DNS_ADDRESSES[@]}"; do + run_delv "@$addr" -t A dual.onlinesign.test + grep -qF "10.0.0.135" "$RUN_OUT" + run_delv "@$addr" -t AAAA dual.onlinesign.test + grep -qF "fd00:dead:beef:cafe::135" "$RUN_OUT" + run_delv "@$addr" -t ANY ipv6.onlinesign.test + grep -qF "fd00:dead:beef:cafe::136" "$RUN_OUT" +done +run resolvectl query dual.onlinesign.test +grep -qF "10.0.0.135" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::135" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" +run resolvectl query ipv6.onlinesign.test +grep -qF "fd00:dead:beef:cafe::136" "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" + +# Check a non-existent domain +# Note: mod-onlinesign utilizes Minimally Covering NSEC Records, hence the +# different response than with "standard" DNSSEC +run dig +dnssec this.does.not.exist.onlinesign.test +grep -qF "status: NOERROR" "$RUN_OUT" +grep -qF "NSEC \\000.this.does.not.exist.onlinesign.test." "$RUN_OUT" +# Check a wildcard record +run resolvectl query -t TXT this.should.be.authenticated.wild.onlinesign.test +grep -qF 'this.should.be.authenticated.wild.onlinesign.test IN TXT "this is an onlinesign wildcard"' "$RUN_OUT" +grep -qF "authenticated: yes" "$RUN_OUT" + +# Resolve via dbus method +TIMESTAMP=$(date '+%F %T') +run busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager ResolveHostname 'isit' 0 secondsub.onlinesign.test 0 0 +grep -qF '10 0 0 134 "secondsub.onlinesign.test"' "$RUN_OUT" +monitor_check_rr "$TIMESTAMP" "secondsub.onlinesign.test IN A 10.0.0.134" + + +: "--- ZONE: untrusted.test (DNSSEC without propagated DS records) ---" +# Issue: https://github.com/systemd/systemd/issues/23955 +# FIXME +resolvectl flush-caches +#run dig +short untrusted.test A untrusted.test AAAA +#grep -qF "10.0.0.121" "$RUN_OUT" +#grep -qF "fd00:dead:beef:cafe::121" "$RUN_OUT" +run resolvectl query untrusted.test +grep -qF "untrusted.test:" "$RUN_OUT" +grep -qF "10.0.0.121" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::121" "$RUN_OUT" +grep -qF "authenticated: no" "$RUN_OUT" +run resolvectl service _mysvc._tcp untrusted.test +grep -qF "myservice.untrusted.test:1234" "$RUN_OUT" +grep -qF "10.0.0.123" "$RUN_OUT" +grep -qF "fd00:dead:beef:cafe::123" "$RUN_OUT" + +# Issue: https://github.com/systemd/systemd/issues/19472 +# 1) Query for a non-existing RR should return NOERROR + NSEC (?), not NXDOMAIN +# FIXME: re-enable once the issue is resolved +#run dig +dnssec AAAA untrusted.test +#grep -qF "status: NOERROR" "$RUN_OUT" +#grep -qE "^untrusted\.test\..+IN\s+NSEC\s+" "$RUN_OUT" +## 2) Query for a non-existing name should return NXDOMAIN, not SERVFAIL +#run dig +dnssec this.does.not.exist.untrusted.test +#grep -qF "status: NXDOMAIN" "$RUN_OUT" + +### Test resolvectl show-cache +run resolvectl show-cache +run resolvectl show-cache --json=short +run resolvectl show-cache --json=pretty + +# Issue: https://github.com/systemd/systemd/issues/29580 (part #1) +dig @127.0.0.54 signed.test + +systemctl stop resolvectl-monitor.service +systemctl stop resolvectl-monitor-json.service + +# Issue: https://github.com/systemd/systemd/issues/29580 (part #2) +# +# Check for any warnings regarding malformed messages +(! journalctl -u resolvectl-monitor.service -u reseolvectl-monitor-json.service -p warning --grep malformed) +# Verify that all queries recorded by `resolvectl monitor --json` produced a valid JSON +# with expected fields +journalctl -p info -o cat _SYSTEMD_UNIT="resolvectl-monitor-json.service" | while read -r line; do + # Check that both "question" and "answer" fields are arrays + # + # The expression is slightly more complicated due to the fact that the "answer" field is optional, + # so we need to select it only if it's present, otherwise the type == "array" check would fail + echo "$line" | jq -e '[. | .question, (select(has("answer")) | .answer) | type == "array"] | all' +done + +# Test serve stale feature and NFTSet= if nftables is installed +if command -v nft >/dev/null; then + ### Test without serve stale feature ### + NFT_FILTER_NAME=dns_port_filter + + drop_dns_outbound_traffic() { + nft add table inet $NFT_FILTER_NAME + nft add chain inet $NFT_FILTER_NAME output \{ type filter hook output priority 0 \; \} + nft add rule inet $NFT_FILTER_NAME output ip daddr 10.0.0.1 udp dport 53 drop + nft add rule inet $NFT_FILTER_NAME output ip daddr 10.0.0.1 tcp dport 53 drop + nft add rule inet $NFT_FILTER_NAME output ip6 daddr fd00:dead:beef:cafe::1 udp dport 53 drop + nft add rule inet $NFT_FILTER_NAME output ip6 daddr fd00:dead:beef:cafe::1 tcp dport 53 drop + } + + run dig stale1.unsigned.test -t A + grep -qE "NOERROR" "$RUN_OUT" + sleep 2 + drop_dns_outbound_traffic + set +e + run dig stale1.unsigned.test -t A + set -eux + grep -qE "no servers could be reached" "$RUN_OUT" + nft flush ruleset + + ### Test TIMEOUT with serve stale feature ### + + mkdir -p /run/systemd/resolved.conf.d + { + echo "[Resolve]" + echo "StaleRetentionSec=1d" + } >/run/systemd/resolved.conf.d/test.conf + ln -svf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf + restart_resolved + + run dig stale1.unsigned.test -t A + grep -qE "NOERROR" "$RUN_OUT" + sleep 2 + drop_dns_outbound_traffic + run dig stale1.unsigned.test -t A + grep -qE "NOERROR" "$RUN_OUT" + grep -qE "10.0.0.112" "$RUN_OUT" + + nft flush ruleset + + ### Test NXDOMAIN with serve stale feature ### + # NXDOMAIN response should replace the cache with NXDOMAIN response + run dig stale1.unsigned.test -t A + grep -qE "NOERROR" "$RUN_OUT" + # Delete stale1 record from zone + knotc zone-begin unsigned.test + knotc zone-unset unsigned.test stale1 A + knotc zone-commit unsigned.test + knotc reload + sleep 2 + run dig stale1.unsigned.test -t A + grep -qE "NXDOMAIN" "$RUN_OUT" + + nft flush ruleset + + ### NFTSet= test + nft add table inet sd_test + nft add set inet sd_test c '{ type cgroupsv2; }' + nft add set inet sd_test u '{ typeof meta skuid; }' + nft add set inet sd_test g '{ typeof meta skgid; }' + + # service + systemd-run --unit test-nft.service --service-type=exec -p DynamicUser=yes \ + -p 'NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g' sleep 10000 + run nft list set inet sd_test c + grep -qF "test-nft.service" "$RUN_OUT" + uid=$(getent passwd test-nft | cut -d':' -f3) + run nft list set inet sd_test u + grep -qF "$uid" "$RUN_OUT" + gid=$(getent passwd test-nft | cut -d':' -f4) + run nft list set inet sd_test g + grep -qF "$gid" "$RUN_OUT" + systemctl stop test-nft.service + + # scope + run systemd-run --scope -u test-nft.scope -p 'NFTSet=cgroup:inet:sd_test:c' nft list set inet sd_test c + grep -qF "test-nft.scope" "$RUN_OUT" + + mkdir -p /run/systemd/system + # socket + { + echo "[Socket]" + echo "ListenStream=12345" + echo "BindToDevice=lo" + echo "NFTSet=cgroup:inet:sd_test:c" + } >/run/systemd/system/test-nft.socket + { + echo "[Service]" + echo "ExecStart=/usr/bin/sleep 10000" + } >/run/systemd/system/test-nft.service + systemctl daemon-reload + systemctl start test-nft.socket + systemctl status test-nft.socket + run nft list set inet sd_test c + grep -qF "test-nft.socket" "$RUN_OUT" + systemctl stop test-nft.socket + rm -f /run/systemd/system/test-nft.{socket,service} + + # slice + mkdir /run/systemd/system/system.slice.d + { + echo "[Slice]" + echo "NFTSet=cgroup:inet:sd_test:c" + } >/run/systemd/system/system.slice.d/00-test-nft.conf + systemctl daemon-reload + run nft list set inet sd_test c + grep -qF "system.slice" "$RUN_OUT" + rm -rf /run/systemd/system/system.slice.d + + nft flush ruleset +else + echo "nftables is not installed. Skipped serve stale feature and NFTSet= tests." +fi + +### Test resolvectl show-server-state ### +run resolvectl show-server-state +grep -qF "10.0.0.1" "$RUN_OUT" +grep -qF "Interface" "$RUN_OUT" + +run resolvectl show-server-state --json=short +grep -qF "10.0.0.1" "$RUN_OUT" +grep -qF "Interface" "$RUN_OUT" + +run resolvectl show-server-state --json=pretty +grep -qF "10.0.0.1" "$RUN_OUT" +grep -qF "Interface" "$RUN_OUT" + +### Test resolvectl statistics ### +run resolvectl statistics +grep -qF "Transactions" "$RUN_OUT" +grep -qF "Cache" "$RUN_OUT" +grep -qF "Failure Transactions" "$RUN_OUT" +grep -qF "DNSSEC Verdicts" "$RUN_OUT" + +run resolvectl statistics --json=short +grep -qF "transactions" "$RUN_OUT" +grep -qF "cache" "$RUN_OUT" +grep -qF "dnssec" "$RUN_OUT" + +run resolvectl statistics --json=pretty +grep -qF "transactions" "$RUN_OUT" +grep -qF "cache" "$RUN_OUT" +grep -qF "dnssec" "$RUN_OUT" + +### Test resolvectl reset-statistics ### +run resolvectl reset-statistics + +run resolvectl reset-statistics --json=pretty + +run resolvectl reset-statistics --json=short + +# Check if resolved exits cleanly. +restart_resolved + +touch /testok diff --git a/test/units/testsuite-76.service b/test/units/testsuite-76.service new file mode 100644 index 0000000..3c8a9e8 --- /dev/null +++ b/test/units/testsuite-76.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-76-SYSCTL + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-76.sh b/test/units/testsuite-76.sh new file mode 100755 index 0000000..855d0ef --- /dev/null +++ b/test/units/testsuite-76.sh @@ -0,0 +1,39 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug + +echo "foo.bar=42" >/tmp/foo.conf +assert_rc 0 /usr/lib/systemd/systemd-sysctl /tmp/foo.conf +assert_rc 1 /usr/lib/systemd/systemd-sysctl --strict /tmp/foo.conf + +echo "-foo.foo=42" >/tmp/foo.conf +assert_rc 0 /usr/lib/systemd/systemd-sysctl /tmp/foo.conf +assert_rc 0 /usr/lib/systemd/systemd-sysctl --strict /tmp/foo.conf + +if ! systemd-detect-virt --quiet --container; then + ip link add hoge type dummy + udevadm wait /sys/class/net/hoge + + cat >/tmp/foo.conf <<EOF +net.ipv4.conf.*.drop_gratuitous_arp=1 +net.ipv4.*.*.bootp_relay=1 +net.ipv4.aaa.*.disable_policy=1 +EOF + + echo 0 >/proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp + echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay + echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + + assert_rc 0 /usr/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/hoge /tmp/foo.conf + assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp)" "1" + assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/bootp_relay)" "1" + assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/disable_policy)" "0" +fi + +touch /testok diff --git a/test/units/testsuite-77-client.sh b/test/units/testsuite-77-client.sh new file mode 100755 index 0000000..0d9487a --- /dev/null +++ b/test/units/testsuite-77-client.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 + +export SYSTEMD_LOG_LEVEL=debug + +assert_eq "$LISTEN_FDS" "1" +assert_eq "$LISTEN_FDNAMES" "socket" +read -r -u 3 text +assert_eq "$text" "Socket" diff --git a/test/units/testsuite-77-run.sh b/test/units/testsuite-77-run.sh new file mode 100755 index 0000000..fadd34d --- /dev/null +++ b/test/units/testsuite-77-run.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 + +export SYSTEMD_LOG_LEVEL=debug + +assert_eq "$LISTEN_FDS" "1" +assert_eq "$LISTEN_FDNAMES" "new-file" +read -r -u 3 text +assert_eq "$text" "New" diff --git a/test/units/testsuite-77-server.socket b/test/units/testsuite-77-server.socket new file mode 100644 index 0000000..4305077 --- /dev/null +++ b/test/units/testsuite-77-server.socket @@ -0,0 +1,6 @@ +[Unit] +Description=TEST-77-OPENFILE server socket + +[Socket] +ListenStream=/tmp/test.sock +Accept=yes diff --git a/test/units/testsuite-77-server@.service b/test/units/testsuite-77-server@.service new file mode 100644 index 0000000..8e99ac8 --- /dev/null +++ b/test/units/testsuite-77-server@.service @@ -0,0 +1,7 @@ +[Unit] +Description=TEST-77-OPENFILE server + +[Service] +ExecStart=echo "Socket" +StandardInput=socket +StandardOutput=socket diff --git a/test/units/testsuite-77.service b/test/units/testsuite-77.service new file mode 100644 index 0000000..6ed8add --- /dev/null +++ b/test/units/testsuite-77.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-77-OPENFILE + +[Service] +OpenFile=/test-77-open.dat:open:read-only +OpenFile=/test-77-file.dat +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-77.sh b/test/units/testsuite-77.sh new file mode 100755 index 0000000..2b85a8c --- /dev/null +++ b/test/units/testsuite-77.sh @@ -0,0 +1,38 @@ +#!/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 + +export SYSTEMD_LOG_LEVEL=debug + +assert_eq "$LISTEN_FDS" "2" +assert_eq "$LISTEN_FDNAMES" "open:test-77-file.dat" +read -r -u 3 text +assert_eq "$text" "Open" +read -r -u 4 text +assert_eq "$text" "File" + +# Test for socket +systemctl start testsuite-77-server.socket +systemd-run -p OpenFile=/tmp/test.sock:socket:read-only \ + --wait \ + --pipe \ + /usr/lib/systemd/tests/testdata/units/testsuite-77-client.sh + +# Tests for D-Bus +diff <(systemctl show -p OpenFile testsuite-77) - <<EOF +OpenFile=/test-77-open.dat:open:read-only +OpenFile=/test-77-file.dat +EOF +echo "New" >/test-77-new-file.dat +systemd-run --wait -p OpenFile=/test-77-new-file.dat:new-file:read-only "$(dirname "$0")"/testsuite-77-run.sh + +assert_rc 202 systemd-run --wait -p OpenFile=/test-77-new-file.dat:new-file:read-only -p OpenFile=/test-77-mssing-file.dat:missing-file:read-only "$(dirname "$0")"/testsuite-77-run.sh + +assert_rc 0 systemd-run --wait -p OpenFile=/test-77-new-file.dat:new-file:read-only -p OpenFile=/test-77-mssing-file.dat:missing-file:read-only,graceful "$(dirname "$0")"/testsuite-77-run.sh + +# End +touch /testok diff --git a/test/units/testsuite-78.service b/test/units/testsuite-78.service new file mode 100644 index 0000000..05f3eff --- /dev/null +++ b/test/units/testsuite-78.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-78-SIGQUEUE + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh diff --git a/test/units/testsuite-78.sh b/test/units/testsuite-78.sh new file mode 100755 index 0000000..46afd3c --- /dev/null +++ b/test/units/testsuite-78.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +if ! env --block-signal=SIGUSR1 true 2> /dev/null ; then + echo "env tool too old, can't block signals, skipping test." >&2 + echo OK >/testok + exit 0 +fi + +systemd-analyze log-level debug + +UNIT="test-sigqueue-$RANDOM.service" + +systemd-run -u "$UNIT" -p Type=notify -p DynamicUser=1 -- env --block-signal=SIGRTMIN+7 systemd-notify --exec --ready \; sleep infinity + +systemctl kill --kill-whom=main --kill-value=4 --signal=SIGRTMIN+7 "$UNIT" +systemctl kill --kill-whom=main --kill-value=4 --signal=SIGRTMIN+7 "$UNIT" +systemctl kill --kill-whom=main --kill-value=7 --signal=SIGRTMIN+7 "$UNIT" +systemctl kill --kill-whom=main --kill-value=16 --signal=SIGRTMIN+7 "$UNIT" +systemctl kill --kill-whom=main --kill-value=32 --signal=SIGRTMIN+7 "$UNIT" +systemctl kill --kill-whom=main --kill-value=16 --signal=SIGRTMIN+7 "$UNIT" + +# We simply check that six signals are queued now. There's no easy way to check +# from shell which ones those are, hence we don't check that. +P=$(systemctl show -P MainPID "$UNIT") + +test "$(grep SigQ: /proc/"$P"/status | cut -d: -f2 | cut -d/ -f1)" -eq 6 + +systemctl stop $UNIT + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-79.service b/test/units/testsuite-79.service new file mode 100644 index 0000000..f2d24df --- /dev/null +++ b/test/units/testsuite-79.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-79-MEMPRESS + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +MemoryAccounting=1 diff --git a/test/units/testsuite-79.sh b/test/units/testsuite-79.sh new file mode 100755 index 0000000..205f7f3 --- /dev/null +++ b/test/units/testsuite-79.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# We not just test if the file exists, but try to read from it, since if +# CONFIG_PSI_DEFAULT_DISABLED is set in the kernel the file will exist and can +# be opened, but any read()s will fail with EOPNOTSUPP, which we want to +# detect. +if ! cat /proc/pressure/memory >/dev/null ; then + echo "kernel too old, has no PSI." >&2 + echo OK >/testok + exit 0 +fi + +systemd-analyze log-level debug + +CGROUP=/sys/fs/cgroup/"$(systemctl show testsuite-79.service -P ControlGroup)" +test -d "$CGROUP" + +if ! test -f "$CGROUP"/memory.pressure ; then + echo "No memory accounting/PSI delegated via cgroup, can't test." >&2 + echo OK >/testok + exit 0 +fi + +UNIT="test-mempress-$RANDOM.service" +SCRIPT="/tmp/mempress-$RANDOM.sh" + +cat >"$SCRIPT" <<'EOF' +#!/bin/bash + +set -ex + +export +id + +test -n "$MEMORY_PRESSURE_WATCH" +test "$MEMORY_PRESSURE_WATCH" != /dev/null +test -w "$MEMORY_PRESSURE_WATCH" + +ls -al "$MEMORY_PRESSURE_WATCH" + +EXPECTED="$(echo -n -e "some 123000 2000000\x00" | base64)" + +test "$EXPECTED" = "$MEMORY_PRESSURE_WRITE" + +EOF + +chmod +x "$SCRIPT" + +systemd-run -u "$UNIT" -p Type=exec -p ProtectControlGroups=1 -p DynamicUser=1 -p MemoryPressureWatch=on -p MemoryPressureThresholdSec=123ms -p BindPaths=$SCRIPT --wait "$SCRIPT" + +rm "$SCRIPT" + +systemd-analyze log-level info + +touch /testok diff --git a/test/units/testsuite-80.service b/test/units/testsuite-80.service new file mode 100644 index 0000000..4c7f5d5 --- /dev/null +++ b/test/units/testsuite-80.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-80-NOTIFYACCESS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-80.sh b/test/units/testsuite-80.sh new file mode 100755 index 0000000..97b222a --- /dev/null +++ b/test/units/testsuite-80.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +mkfifo /tmp/syncfifo1 /tmp/syncfifo2 + +sync_in() { + read -r x < /tmp/syncfifo1 + test "$x" = "$1" +} + +sync_out() { + echo "$1" > /tmp/syncfifo2 +} + +export SYSTEMD_LOG_LEVEL=debug + +systemctl --no-block start notify.service + +sync_in a + +assert_eq "$(systemctl show notify.service -p NotifyAccess --value)" "all" +assert_eq "$(systemctl show notify.service -p StatusText --value)" "Test starts" + +sync_out b +sync_in c + +assert_eq "$(systemctl show notify.service -p NotifyAccess --value)" "main" +assert_eq "$(systemctl show notify.service -p StatusText --value)" "Sending READY=1 in an unprivileged process" +assert_rc 3 systemctl --quiet is-active notify.service + +sync_out d +sync_in e + +systemctl --quiet is-active notify.service +assert_eq "$(systemctl show notify.service -p StatusText --value)" "OK" +assert_eq "$(systemctl show notify.service -p NotifyAccess --value)" "none" + +systemctl stop notify.service +assert_eq "$(systemctl show notify.service -p NotifyAccess --value)" "all" + +rm /tmp/syncfifo1 /tmp/syncfifo2 + +# Now test basic fdstore behaviour + +MYSCRIPT="/tmp/myscript$RANDOM.sh" +cat >> "$MYSCRIPT" <<'EOF' +#!/usr/bin/env bash +set -eux +set -o pipefail +test "$FDSTORE" -eq 7 +N="/tmp/$RANDOM" +echo $RANDOM > "$N" +systemd-notify --fd=4 --fdname=quux --pid=parent 4< "$N" +rm "$N" +systemd-notify --ready +exec sleep infinity +EOF + +chmod +x "$MYSCRIPT" + +MYUNIT="myunit$RANDOM.service" +systemd-run -u "$MYUNIT" -p Type=notify -p FileDescriptorStoreMax=7 "$MYSCRIPT" + +test "$(systemd-analyze fdstore "$MYUNIT" | wc -l)" -eq 2 +systemd-analyze fdstore "$MYUNIT" --json=short +systemd-analyze fdstore "$MYUNIT" --json=short | grep -P -q '\[{"fdname":"quux","type":.*,"devno":\[.*\],"inode":.*,"rdevno":null,"path":"/tmp/.*","flags":"ro"}\]' + +systemctl stop "$MYUNIT" +rm "$MYSCRIPT" + +systemd-analyze log-level debug + +# Test fdstore pinning (this will pull in fdstore-pin.service fdstore-nopin.service) +systemctl start fdstore-pin.target + +assert_eq "$(systemctl show fdstore-pin.service -P FileDescriptorStorePreserve)" yes +assert_eq "$(systemctl show fdstore-nopin.service -P FileDescriptorStorePreserve)" restart +assert_eq "$(systemctl show fdstore-pin.service -P SubState)" running +assert_eq "$(systemctl show fdstore-nopin.service -P SubState)" running +assert_eq "$(systemctl show fdstore-pin.service -P NFileDescriptorStore)" 1 +assert_eq "$(systemctl show fdstore-nopin.service -P NFileDescriptorStore)" 1 + +# The file descriptor store should survive service restarts +systemctl restart fdstore-pin.service fdstore-nopin.service + +assert_eq "$(systemctl show fdstore-pin.service -P NFileDescriptorStore)" 1 +assert_eq "$(systemctl show fdstore-nopin.service -P NFileDescriptorStore)" 1 +assert_eq "$(systemctl show fdstore-pin.service -P SubState)" running +assert_eq "$(systemctl show fdstore-nopin.service -P SubState)" running + +# It should not survive the service stop plus a later start (unless pinned) +systemctl stop fdstore-pin.service fdstore-nopin.service + +assert_eq "$(systemctl show fdstore-pin.service -P NFileDescriptorStore)" 1 +assert_eq "$(systemctl show fdstore-nopin.service -P NFileDescriptorStore)" 0 +assert_eq "$(systemctl show fdstore-pin.service -P SubState)" dead-resources-pinned +assert_eq "$(systemctl show fdstore-nopin.service -P SubState)" dead + +systemctl start fdstore-pin.service fdstore-nopin.service + +assert_eq "$(systemctl show fdstore-pin.service -P NFileDescriptorStore)" 1 +assert_eq "$(systemctl show fdstore-nopin.service -P NFileDescriptorStore)" 0 +assert_eq "$(systemctl show fdstore-pin.service -P SubState)" running +assert_eq "$(systemctl show fdstore-nopin.service -P SubState)" running + +systemctl stop fdstore-pin.service fdstore-nopin.service + +assert_eq "$(systemctl show fdstore-pin.service -P NFileDescriptorStore)" 1 +assert_eq "$(systemctl show fdstore-nopin.service -P NFileDescriptorStore)" 0 +assert_eq "$(systemctl show fdstore-pin.service -P SubState)" dead-resources-pinned +assert_eq "$(systemctl show fdstore-nopin.service -P SubState)" dead + +systemctl clean fdstore-pin.service --what=fdstore + +assert_eq "$(systemctl show fdstore-pin.service -P NFileDescriptorStore)" 0 +assert_eq "$(systemctl show fdstore-nopin.service -P NFileDescriptorStore)" 0 +assert_eq "$(systemctl show fdstore-pin.service -P SubState)" dead +assert_eq "$(systemctl show fdstore-nopin.service -P SubState)" dead + +touch /testok diff --git a/test/units/testsuite-81.debug-generator.sh b/test/units/testsuite-81.debug-generator.sh new file mode 100755 index 0000000..fddf85a --- /dev/null +++ b/test/units/testsuite-81.debug-generator.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +set -eux +set -o pipefail + +# shellcheck source=test/units/generator-utils.sh +. "$(dirname "$0")/generator-utils.sh" + +GENERATOR_BIN="/usr/lib/systemd/system-generators/systemd-debug-generator" +OUT_DIR="$(mktemp -d /tmp/debug-generator.XXX)" + +at_exit() { + rm -frv "${OUT_DIR:?}" +} + +trap at_exit EXIT + +test -x "${GENERATOR_BIN:?}" + +# Potential FIXME: +# - debug-generator should gracefully handle duplicated mask/wants +# - also, handle gracefully empty mask/wants +ARGS=( + "systemd.mask=masked-no-suffix" + "systemd.mask=masked.service" + "systemd.mask=masked.socket" + "systemd.wants=wanted-no-suffix" + "systemd.wants=wanted.service" + "systemd.wants=wanted.mount" + "rd.systemd.mask=masked-initrd.service" + "rd.systemd.wants=wanted-initrd.service" +) + +# Regular (non-initrd) scenario +# +: "debug-shell: regular" +CMDLINE="ro root=/ ${ARGS[*]} rd.systemd.debug_shell" +SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_eq "$OUT_DIR/early/masked-no-suffix.service" /dev/null +link_eq "$OUT_DIR/early/masked.service" /dev/null +link_eq "$OUT_DIR/early/masked.socket" /dev/null +link_endswith "$OUT_DIR/early/default.target.wants/wanted-no-suffix.service" /lib/systemd/system/wanted-no-suffix.service +link_endswith "$OUT_DIR/early/default.target.wants/wanted.service" /lib/systemd/system/wanted.service +link_endswith "$OUT_DIR/early/default.target.wants/wanted.mount" /lib/systemd/system/wanted.mount +# Following stuff should be ignored, as it's prefixed with rd. +test ! -h "$OUT_DIR/early/masked-initrd.service" +test ! -h "$OUT_DIR/early/default.target.wants/wants-initrd.service" +test ! -h "$OUT_DIR/early/default.target.wants/debug-shell.service" +test ! -d "$OUT_DIR/early/initrd.target.wants" + +# Let's re-run the generator with systemd.debug_shell that should be honored +: "debug-shell: regular + systemd.debug_shell" +CMDLINE="$CMDLINE systemd.debug_shell" +SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_endswith "$OUT_DIR/early/default.target.wants/debug-shell.service" /lib/systemd/system/debug-shell.service + +# Same thing, but with custom tty +: "debug-shell: regular + systemd.debug_shell=/dev/tty666" +CMDLINE="$CMDLINE systemd.debug_shell=/dev/tty666" +SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_endswith "$OUT_DIR/early/default.target.wants/debug-shell.service" /lib/systemd/system/debug-shell.service +grep -F "/dev/tty666" "$OUT_DIR/early/debug-shell.service.d/50-tty.conf" + +# Now override the default target via systemd.unit= +: "debug-shell: regular + systemd.unit=" +CMDLINE="$CMDLINE systemd.unit=my-fancy.target" +SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_eq "$OUT_DIR/early/masked-no-suffix.service" /dev/null +link_eq "$OUT_DIR/early/masked.service" /dev/null +link_eq "$OUT_DIR/early/masked.socket" /dev/null +link_endswith "$OUT_DIR/early/my-fancy.target.wants/wanted-no-suffix.service" /lib/systemd/system/wanted-no-suffix.service +link_endswith "$OUT_DIR/early/my-fancy.target.wants/wanted.service" /lib/systemd/system/wanted.service +link_endswith "$OUT_DIR/early/my-fancy.target.wants/wanted.mount" /lib/systemd/system/wanted.mount +link_endswith "$OUT_DIR/early/my-fancy.target.wants/debug-shell.service" /lib/systemd/system/debug-shell.service +test ! -d "$OUT_DIR/early/default.target.wants" + + +# Initrd scenario +: "debug-shell: initrd" +CMDLINE="ro root=/ ${ARGS[*]} systemd.debug_shell" +SYSTEMD_IN_INITRD=1 SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_eq "$OUT_DIR/early/masked-initrd.service" /dev/null +link_endswith "$OUT_DIR/early/initrd.target.wants/wanted-initrd.service" /lib/systemd/system/wanted-initrd.service +# The non-initrd stuff (i.e. without the rd. suffix) should be ignored in +# this case +test ! -h "$OUT_DIR/early/masked-no-suffix.service" +test ! -h "$OUT_DIR/early/masked.service" +test ! -h "$OUT_DIR/early/masked.socket" +test ! -h "$OUT_DIR/early/initrd.target.wants/debug-shell.service" +test ! -d "$OUT_DIR/early/default.target.wants" + +# Again, but with rd.systemd.debug_shell +: "debug-shell: initrd + rd.systemd.debug_shell" +CMDLINE="$CMDLINE rd.systemd.debug_shell" +SYSTEMD_IN_INITRD=1 SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_endswith "$OUT_DIR/early/initrd.target.wants/debug-shell.service" /lib/systemd/system/debug-shell.service + +# Override the default target +: "debug-shell: initrd + rd.systemd.unit" +CMDLINE="$CMDLINE rd.systemd.unit=my-fancy-initrd.target" +SYSTEMD_IN_INITRD=1 SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_eq "$OUT_DIR/early/masked-initrd.service" /dev/null +link_endswith "$OUT_DIR/early/my-fancy-initrd.target.wants/wanted-initrd.service" /lib/systemd/system/wanted-initrd.service +test ! -d "$OUT_DIR/early/initrd.target.wants" diff --git a/test/units/testsuite-81.environment-d-generator.sh b/test/units/testsuite-81.environment-d-generator.sh new file mode 100755 index 0000000..5bc3978 --- /dev/null +++ b/test/units/testsuite-81.environment-d-generator.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +set -eux +set -o pipefail + +# shellcheck source=test/units/generator-utils.sh +. "$(dirname "$0")/generator-utils.sh" + +GENERATOR_BIN="/usr/lib/systemd/user-environment-generators/30-systemd-environment-d-generator" +CONFIG_FILE="/run/environment.d/99-test.conf" +OUT_FILE="$(mktemp)" + +at_exit() { + set +e + rm -frv "${CONFIG_FILE:?}" "${OUT_FILE:?}" + systemctl -M testuser@.host --user daemon-reload +} + +trap at_exit EXIT + +test -x "${GENERATOR_BIN:?}" +mkdir -p /run/environment.d/ + +cat >"$CONFIG_FILE" <<EOF + +\t\n\t +3 += + = +INVALID +ALSO_INVALID= +EMPTY_INVALID="" +3_INVALID=foo +xxxx xx xxxxxx +# This is a comment +$(printf "%.0sx" {0..4096})= +SIMPLE=foo +REF=\$SIMPLE +ALSO_REF=\${SIMPLE} +DEFAULT="\${NONEXISTENT:-default value}" +ALTERNATE="\${SIMPLE:+alternate value}" +LIST=foo,bar,baz +SIMPLE=redefined +UNASSIGNED=\$FOO_BAR_BAZ +VERY_LONG="very $(printf "%.0sx" {0..4096})= long string" +EOF + +# Source env assignments from a file and check them - do this in a subshell +# to not pollute the test environment +check_environment() {( + # shellcheck source=/dev/null + source "${1:?}" + + [[ "$SIMPLE" == "redefined" ]] + [[ "$REF" == "foo" ]] + [[ "$ALSO_REF" == "foo" ]] + [[ "$DEFAULT" == "default value" ]] + [[ "$ALTERNATE" == "alternate value" ]] + [[ "$LIST" == "foo,bar,baz" ]] + [[ "$VERY_LONG" =~ ^very\ ]] + [[ "$VERY_LONG" =~ \ long\ string$ ]] + [[ -z "$UNASSIGNED" ]] + [[ ! -v INVALID ]] + [[ ! -v ALSO_INVALID ]] + [[ ! -v EMPTY_INVALID ]] + [[ ! -v 3_INVALID ]] +)} + +# Check the output by directly calling the generator +"$GENERATOR_BIN" | tee "$OUT_FILE" +check_environment "$OUT_FILE" +: >"$OUT_FILE" + +# Check if the generator is correctly called in a user session +systemctl -M testuser@.host --user daemon-reload +systemctl -M testuser@.host --user show-environment | tee "$OUT_FILE" +check_environment "$OUT_FILE" + +(! "$GENERATOR_BIN" foo) diff --git a/test/units/testsuite-81.fstab-generator.sh b/test/units/testsuite-81.fstab-generator.sh new file mode 100755 index 0000000..50c4b2f --- /dev/null +++ b/test/units/testsuite-81.fstab-generator.sh @@ -0,0 +1,406 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235,SC2233 +set -eux +set -o pipefail + +# shellcheck source=test/units/generator-utils.sh +. "$(dirname "$0")/generator-utils.sh" + +GENERATOR_BIN="/usr/lib/systemd/system-generators/systemd-fstab-generator" +NETWORK_FS_RX="^(afs|ceph|cifs|gfs|gfs2|ncp|ncpfs|nfs|nfs4|ocfs2|orangefs|pvfs2|smb3|smbfs|davfs|glusterfs|lustre|sshfs)$" +OUT_DIR="$(mktemp -d /tmp/fstab-generator.XXX)" +FSTAB="$(mktemp)" + +at_exit() { + rm -fr "${OUT_DIR:?}" "${FSTAB:?}" +} + +trap at_exit EXIT + +test -x "${GENERATOR_BIN:?}" + +FSTAB_GENERAL=( + # Valid entries + "/dev/test2 /nofail ext4 nofail 0 0" + "/dev/test3 /regular btrfs defaults 0 0" + "/dev/test4 /x-systemd.requires xfs x-systemd.requires=foo.service 0 0" + "/dev/test5 /x-systemd.before-after xfs x-systemd.before=foo.service,x-systemd.after=bar.mount 0 0" + "/dev/test6 /x-systemd.wanted-required-by xfs x-systemd.wanted-by=foo.service,x-systemd.required-by=bar.device 0 0" + "/dev/test7 /x-systemd.requires-mounts-for xfs x-systemd.requires-mounts-for=/foo/bar/baz 0 0" + "/dev/test8 /x-systemd.automount-idle-timeout vfat x-systemd.automount,x-systemd.idle-timeout=50s 0 0" + "/dev/test9 /x-systemd.makefs xfs x-systemd.makefs 0 0" + "/dev/test10 /x-systemd.growfs xfs x-systemd.growfs 0 0" + "/dev/test11 /_netdev ext4 defaults,_netdev 0 0" + "/dev/test12 /_rwonly ext4 x-systemd.rw-only 0 0" + "/dev/test13 /chaos1 zfs x-systemd.rw-only,x-systemd.requires=hello.service,x-systemd.after=my.device 0 0" + "/dev/test14 /chaos2 zfs x.systemd.wanted-by=foo.service,x-systemd.growfs,x-systemd.makefs 0 0" + "/dev/test15 /fstype/auto auto defaults 0 0" + "/dev/test16 /fsck/me ext4 defaults 0 1" + "/dev/test17 /also/fsck/me ext4 defaults,x-systemd.requires-mounts-for=/var/lib/foo 0 99" + "/dev/test18 /swap swap defaults 0 0" + "/dev/test19 /swap/makefs swap defaults,x-systemd.makefs 0 0" + "/dev/test20 /var xfs defaults,x-systemd.device-timeout=1h 0 0" + "/dev/test21 /usr ext4 defaults 0 1" + "/dev/test22 /initrd/mount ext2 defaults,x-systemd.rw-only,x-initrd.mount 0 1" + "/dev/test23 /initrd/mount/nofail ext3 defaults,nofail,x-initrd.mount 0 1" + "/dev/test24 /initrd/mount/deps ext4 x-initrd.mount,x-systemd.before=early.service,x-systemd.after=late.service 0 1" + + # Incomplete, but valid entries + "/dev/incomplete1 /incomplete1" + "/dev/incomplete2 /incomplete2 ext4" + "/dev/incomplete3 /incomplete3 ext4 defaults" + "/dev/incomplete4 /incomplete4 ext4 defaults 0" + + # Remote filesystems + "/dev/remote1 /nfs nfs bg 0 0" + "/dev/remote2 /nfs4 nfs4 bg 0 0" + "bar.tld:/store /remote/storage nfs ro,x-systemd.wanted-by=store.service 0 0" + "user@host.tld:/remote/dir /remote/top-secret sshfs rw,x-systemd.before=naughty.service 0 0" + "foo.tld:/hello /hello/world ceph defaults 0 0" + "//192.168.0.1/storage /cifs-storage cifs automount,nofail 0 0" +) + +FSTAB_GENERAL_ROOT=( + # rootfs with bunch of options we should ignore and fsck enabled + "/dev/test1 / ext4 noauto,nofail,x-systemd.automount,x-systemd.wanted-by=foo,x-systemd.required-by=bar 0 1" + "${FSTAB_GENERAL[@]}" +) + +FSTAB_MINIMAL=( + "/dev/loop1 /foo/bar ext3 defaults 0 0" +) + +FSTAB_DUPLICATE=( + "/dev/dup1 / ext4 defaults 0 1" + "/dev/dup2 / ext4 defaults,x-systemd.requires=foo.mount 0 2" +) + +FSTAB_INVALID=( + # Ignored entries + "/dev/ignored1 /sys/fs/cgroup/foo ext4 defaults 0 0" + "/dev/ignored2 /sys/fs/selinux ext4 defaults 0 0" + "/dev/ignored3 /dev/console ext4 defaults 0 0" + "/dev/ignored4 /proc/kmsg ext4 defaults 0 0" + "/dev/ignored5 /proc/sys ext4 defaults 0 0" + "/dev/ignored6 /proc/sys/kernel/random/boot_id ext4 defaults 0 0" + "/dev/ignored7 /run/host ext4 defaults 0 0" + "/dev/ignored8 /run/host/foo ext4 defaults 0 0" + "/dev/ignored9 /autofs autofs defaults 0 0" + "/dev/invalid1 not-a-path ext4 defaults 0 0" + "" + "/dev/invalid1" + " " + "\\" + "$" +) + +check_fstab_mount_units() { + local what where fstype opts passno unit + local item opt split_options filtered_options supp service device arg + local array_name="${1:?}" + local out_dir="${2:?}/normal" + # Get a reference to the array from its name + local -n fstab_entries="$array_name" + + # Running the checks in a container is pretty much useless, since we don't + # generate any mounts, but don't skip the whole test to test the "skip" + # paths as well + in_container && return 0 + + for item in "${fstab_entries[@]}"; do + # Don't use a pipe here, as it would make the variables out of scope + read -r what where fstype opts _ passno <<< "$item" + + # Skip non-initrd mounts in initrd + if in_initrd_host && ! [[ "$opts" =~ x-initrd.mount ]]; then + continue + fi + + if [[ "$fstype" == swap ]]; then + unit="$(systemd-escape --suffix=swap --path "${what:?}")" + cat "$out_dir/$unit" + + grep -qE "^What=$what$" "$out_dir/$unit" + if [[ "$opts" != defaults ]]; then + grep -qE "^Options=$opts$" "$out_dir/$unit" + fi + + if [[ "$opts" =~ x-systemd.makefs ]]; then + service="$(systemd-escape --template=systemd-mkswap@.service --path "$what")" + test -e "$out_dir/$service" + fi + + continue + fi + + # If we're parsing host's fstab in initrd, prefix all mount targets + # with /sysroot + in_initrd_host && where="/sysroot${where:?}" + unit="$(systemd-escape --suffix=mount --path "${where:?}")" + cat "$out_dir/$unit" + + # Check the general stuff + grep -qE "^What=$what$" "$out_dir/$unit" + grep -qE "^Where=$where$" "$out_dir/$unit" + if [[ -n "$fstype" ]] && [[ "$fstype" != auto ]]; then + grep -qE "^Type=$fstype$" "$out_dir/$unit" + fi + if [[ -n "$opts" ]] && [[ "$opts" != defaults ]]; then + # Some options are not propagated to the generated unit + if [[ "$where" == / ]]; then + filtered_options="$(opt_filter "$opts" "(noauto|nofail|x-systemd.(wanted-by=|required-by=|automount|device-timeout=))")" + else + filtered_options="$(opt_filter "$opts" "^x-systemd.device-timeout=")" + fi + + if [[ "${filtered_options[*]}" != defaults ]]; then + grep -qE "^Options=.*$filtered_options.*$" "$out_dir/$unit" + fi + fi + + if ! [[ "$opts" =~ (noauto|x-systemd.(wanted-by=|required-by=|automount)) ]]; then + # We don't create the Requires=/Wants= symlinks for noauto/automount mounts + # and for mounts that use x-systemd.wanted-by=/required-by= + if in_initrd_host; then + if [[ "$where" == / ]] || ! [[ "$opts" =~ nofail ]]; then + link_eq "$out_dir/initrd-fs.target.requires/$unit" "../$unit" + else + link_eq "$out_dir/initrd-fs.target.wants/$unit" "../$unit" + fi + elif [[ "$fstype" =~ $NETWORK_FS_RX || "$opts" =~ _netdev ]]; then + # Units with network filesystems should have a Requires= dependency + # on the remote-fs.target, unless they use nofail or are an nfs "bg" + # mounts, in which case the dependency is downgraded to Wants= + if [[ "$opts" =~ nofail ]] || [[ "$fstype" =~ ^(nfs|nfs4) && "$opts" =~ bg ]]; then + link_eq "$out_dir/remote-fs.target.wants/$unit" "../$unit" + else + link_eq "$out_dir/remote-fs.target.requires/$unit" "../$unit" + fi + else + # Similarly, local filesystems should have a Requires= dependency on + # the local-fs.target, unless they use nofail, in which case the + # dependency is downgraded to Wants=. Rootfs is a special case, + # since we always ignore nofail there + if [[ "$where" == / ]] || ! [[ "$opts" =~ nofail ]]; then + link_eq "$out_dir/local-fs.target.requires/$unit" "../$unit" + else + link_eq "$out_dir/local-fs.target.wants/$unit" "../$unit" + fi + fi + fi + + if [[ "${passno:=0}" -ne 0 ]]; then + # Generate systemd-fsck@.service dependencies, if applicable + if in_initrd && [[ "$where" == / || "$where" == /usr ]]; then + continue + fi + + if [[ "$where" == / ]]; then + link_endswith "$out_dir/local-fs.target.wants/systemd-fsck-root.service" "/lib/systemd/system/systemd-fsck-root.service" + else + service="$(systemd-escape --template=systemd-fsck@.service --path "$what")" + grep -qE "^After=$service$" "$out_dir/$unit" + if [[ "$where" == /usr ]]; then + grep -qE "^Wants=$service$" "$out_dir/$unit" + else + grep -qE "^Requires=$service$" "$out_dir/$unit" + fi + fi + fi + + # Check various x-systemd options + # + # First, split them into an array to make splitting them even further + # easier + IFS="," read -ra split_options <<< "$opts" + # and process them one by one. + # + # Note: the "machinery" below might (and probably does) miss some + # combinations of supported options, so tread carefully + for opt in "${split_options[@]}"; do + if [[ "$opt" =~ ^x-systemd.requires= ]]; then + service="$(opt_get_arg "$opt")" + grep -qE "^Requires=$service$" "$out_dir/$unit" + grep -qE "^After=$service$" "$out_dir/$unit" + elif [[ "$opt" =~ ^x-systemd.before= ]]; then + service="$(opt_get_arg "$opt")" + grep -qE "^Before=$service$" "$out_dir/$unit" + elif [[ "$opt" =~ ^x-systemd.after= ]]; then + service="$(opt_get_arg "$opt")" + grep -qE "^After=$service$" "$out_dir/$unit" + elif [[ "$opt" =~ ^x-systemd.wanted-by= ]]; then + service="$(opt_get_arg "$opt")" + if [[ "$where" == / ]]; then + # This option is ignored for rootfs mounts + (! link_eq "$out_dir/$service.wants/$unit" "../$unit") + else + link_eq "$out_dir/$service.wants/$unit" "../$unit" + fi + elif [[ "$opt" =~ ^x-systemd.required-by= ]]; then + service="$(opt_get_arg "$opt")" + if [[ "$where" == / ]]; then + # This option is ignored for rootfs mounts + (! link_eq "$out_dir/$service.requires/$unit" "../$unit") + else + link_eq "$out_dir/$service.requires/$unit" "../$unit" + fi + elif [[ "$opt" =~ ^x-systemd.requires-mounts-for= ]]; then + arg="$(opt_get_arg "$opt")" + grep -qE "^RequiresMountsFor=$arg$" "$out_dir/$unit" + elif [[ "$opt" == x-systemd.device-bound ]]; then + # This is implied for fstab mounts + : + elif [[ "$opt" == x-systemd.automount ]]; then + # The $unit should have an accompanying automount unit + supp="$(systemd-escape --suffix=automount --path "$where")" + if [[ "$where" == / ]]; then + # This option is ignored for rootfs mounts + test ! -e "$out_dir/$supp" + (! link_eq "$out_dir/local-fs.target.requires/$supp" "../$supp") + else + test -e "$out_dir/$supp" + link_eq "$out_dir/local-fs.target.requires/$supp" "../$supp" + fi + elif [[ "$opt" =~ ^x-systemd.idle-timeout= ]]; then + # The timeout applies to the automount unit, not the original + # mount one + arg="$(opt_get_arg "$opt")" + supp="$(systemd-escape --suffix=automount --path "$where")" + grep -qE "^TimeoutIdleSec=$arg$" "$out_dir/$supp" + elif [[ "$opt" =~ ^x-systemd.device-timeout= ]]; then + arg="$(opt_get_arg "$opt")" + device="$(systemd-escape --suffix=device --path "$what")" + grep -qE "^JobRunningTimeoutSec=$arg$" "$out_dir/${device}.d/50-device-timeout.conf" + elif [[ "$opt" == x-systemd.makefs ]]; then + service="$(systemd-escape --template=systemd-makefs@.service --path "$what")" + test -e "$out_dir/$service" + link_eq "$out_dir/${unit}.requires/$service" "../$service" + elif [[ "$opt" == x-systemd.rw-only ]]; then + grep -qE "^ReadWriteOnly=yes$" "$out_dir/$unit" + elif [[ "$opt" == x-systemd.growfs ]]; then + service="$(systemd-escape --template=systemd-growfs@.service --path "$where")" + link_endswith "$out_dir/${unit}.wants/$service" "/lib/systemd/system/systemd-growfs@.service" + elif [[ "$opt" == bg ]] && [[ "$fstype" =~ ^(nfs|nfs4)$ ]]; then + # We "convert" nfs bg mounts to fg, so we can do the job-control + # ourselves + grep -qE "^Options=.*\bx-systemd.mount-timeout=infinity\b" "$out_dir/$unit" + grep -qE "^Options=.*\bfg\b.*" "$out_dir/$unit" + elif [[ "$opt" =~ ^x-systemd\. ]]; then + echo >&2 "Unhandled mount option: $opt" + exit 1 + fi + done + done +} + +: "fstab-generator: regular" +printf "%s\n" "${FSTAB_GENERAL_ROOT[@]}" >"$FSTAB" +cat "$FSTAB" +SYSTEMD_FSTAB="$FSTAB" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +check_fstab_mount_units FSTAB_GENERAL_ROOT "$OUT_DIR" + +# Skip the rest when running in a container, as it makes little sense to check +# initrd-related stuff there and fstab-generator might have a bit strange +# behavior during certain tests, like https://github.com/systemd/systemd/issues/27156 +if in_container; then + echo "Running in a container, skipping the rest of the fstab-generator tests..." + exit 0 +fi + +# In this mode we treat the entries as "regular" ones +: "fstab-generator: initrd - initrd fstab" +printf "%s\n" "${FSTAB_GENERAL[@]}" >"$FSTAB" +cat "$FSTAB" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB="$FSTAB" SYSTEMD_SYSROOT_FSTAB=/dev/null run_and_list "$GENERATOR_BIN" "$OUT_DIR" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB="$FSTAB" SYSTEMD_SYSROOT_FSTAB=/dev/null check_fstab_mount_units FSTAB_GENERAL "$OUT_DIR" + +# In this mode we prefix the mount target with /sysroot and ignore all mounts +# that don't have the x-initrd.mount flag +: "fstab-generator: initrd - host fstab" +printf "%s\n" "${FSTAB_GENERAL_ROOT[@]}" >"$FSTAB" +cat "$FSTAB" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB=/dev/null SYSTEMD_SYSROOT_FSTAB="$FSTAB" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB=/dev/null SYSTEMD_SYSROOT_FSTAB="$FSTAB" check_fstab_mount_units FSTAB_GENERAL_ROOT "$OUT_DIR" + +# Check the default stuff that we (almost) always create in initrd +: "fstab-generator: initrd default" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB=/dev/null SYSTEMD_SYSROOT_FSTAB=/dev/null run_and_list "$GENERATOR_BIN" "$OUT_DIR" +test -e "$OUT_DIR/normal/sysroot.mount" +test -e "$OUT_DIR/normal/systemd-fsck-root.service" +link_eq "$OUT_DIR/normal/initrd-root-fs.target.requires/sysroot.mount" "../sysroot.mount" +link_eq "$OUT_DIR/normal/initrd-root-fs.target.requires/sysroot.mount" "../sysroot.mount" + +: "fstab-generator: run as systemd-sysroot-fstab-check in initrd" +ln -svf "$GENERATOR_BIN" /tmp/systemd-sysroot-fstab-check +(! /tmp/systemd-sysroot-fstab-check foo) +(! SYSTEMD_IN_INITRD=0 /tmp/systemd-sysroot-fstab-check) +printf "%s\n" "${FSTAB_GENERAL[@]}" >"$FSTAB" +SYSTEMD_IN_INITRD=1 SYSTEMD_SYSROOT_FSTAB="$FSTAB" /tmp/systemd-sysroot-fstab-check + +: "fstab-generator: duplicate" +printf "%s\n" "${FSTAB_DUPLICATE[@]}" >"$FSTAB" +cat "$FSTAB" +(! SYSTEMD_FSTAB="$FSTAB" run_and_list "$GENERATOR_BIN" "$OUT_DIR") + +: "fstab-generator: invalid" +printf "%s\n" "${FSTAB_INVALID[@]}" >"$FSTAB" +cat "$FSTAB" +# Don't care about the exit code here +SYSTEMD_FSTAB="$FSTAB" run_and_list "$GENERATOR_BIN" "$OUT_DIR" || : +# No mounts should get created here +[[ "$(find "$OUT_DIR" -name "*.mount" | wc -l)" -eq 0 ]] + +: "fstab-generator: kernel args - fstab=0" +printf "%s\n" "${FSTAB_MINIMAL[@]}" >"$FSTAB" +SYSTEMD_FSTAB="$FSTAB" SYSTEMD_PROC_CMDLINE="fstab=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +(! SYSTEMD_FSTAB="$FSTAB" check_fstab_mount_units FSTAB_MINIMAL "$OUT_DIR") +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB="$FSTAB" SYSTEMD_PROC_CMDLINE="fstab=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +(! SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB="$FSTAB" check_fstab_mount_units FSTAB_MINIMAL "$OUT_DIR") + +: "fstab-generator: kernel args - rd.fstab=0" +printf "%s\n" "${FSTAB_MINIMAL[@]}" >"$FSTAB" +SYSTEMD_FSTAB="$FSTAB" SYSTEMD_PROC_CMDLINE="rd.fstab=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +SYSTEMD_FSTAB="$FSTAB" check_fstab_mount_units FSTAB_MINIMAL "$OUT_DIR" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB="$FSTAB" SYSTEMD_PROC_CMDLINE="rd.fstab=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +(! SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB="$FSTAB" check_fstab_mount_units FSTAB_MINIMAL "$OUT_DIR") + +: "fstab-generator: kernel args - systemd.swap=0" +printf "%s\n" "${FSTAB_GENERAL_ROOT[@]}" >"$FSTAB" +cat "$FSTAB" +SYSTEMD_FSTAB="$FSTAB" SYSTEMD_PROC_CMDLINE="systemd.swap=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +# No swap units should get created here +[[ "$(find "$OUT_DIR" -name "*.swap" | wc -l)" -eq 0 ]] + +# Possible TODO +# - combine the rootfs & usrfs arguments and mix them with fstab entries +# - systemd.volatile= +: "fstab-generator: kernel args - root= + rootfstype= + rootflags=" +# shellcheck disable=SC2034 +EXPECTED_FSTAB=( + "/dev/disk/by-label/rootfs / ext4 noexec,ro 0 1" +) +CMDLINE="root=LABEL=rootfs rootfstype=ext4 rootflags=noexec" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB=/dev/null SYSTEMD_SYSROOT_FSTAB=/dev/null SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +# The /proc/cmdline here is a dummy value to tell the in_initrd_host() function +# we're parsing host's fstab, but it's all on the kernel cmdline instead +SYSTEMD_IN_INITRD=1 SYSTEMD_SYSROOT_FSTAB=/proc/cmdline check_fstab_mount_units EXPECTED_FSTAB "$OUT_DIR" + +# This is a very basic sanity test that involves manual checks, since adding it +# to the check_fstab_mount_units() function would make it way too complex +# (yet another possible TODO) +: "fstab-generator: kernel args - mount.usr= + mount.usrfstype= + mount.usrflags=" +CMDLINE="mount.usr=UUID=be780f43-8803-4a76-9732-02ceda6e9808 mount.usrfstype=ext4 mount.usrflags=noexec,nodev" +SYSTEMD_IN_INITRD=1 SYSTEMD_FSTAB=/dev/null SYSTEMD_SYSROOT_FSTAB=/dev/null SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +cat "$OUT_DIR/normal/sysroot-usr.mount" "$OUT_DIR/normal/sysusr-usr.mount" +# The general idea here is to mount the device to /sysusr/usr and then +# bind-mount /sysusr/usr to /sysroot/usr +grep -qE "^What=/dev/disk/by-uuid/be780f43-8803-4a76-9732-02ceda6e9808$" "$OUT_DIR/normal/sysusr-usr.mount" +grep -qE "^Where=/sysusr/usr$" "$OUT_DIR/normal/sysusr-usr.mount" +grep -qE "^Type=ext4$" "$OUT_DIR/normal/sysusr-usr.mount" +grep -qE "^Options=noexec,nodev,ro$" "$OUT_DIR/normal/sysusr-usr.mount" +link_eq "$OUT_DIR/normal/initrd-usr-fs.target.requires/sysusr-usr.mount" "../sysusr-usr.mount" +grep -qE "^What=/sysusr/usr$" "$OUT_DIR/normal/sysroot-usr.mount" +grep -qE "^Where=/sysroot/usr$" "$OUT_DIR/normal/sysroot-usr.mount" +grep -qE "^Options=bind$" "$OUT_DIR/normal/sysroot-usr.mount" +link_eq "$OUT_DIR/normal/initrd-fs.target.requires/sysroot-usr.mount" "../sysroot-usr.mount" diff --git a/test/units/testsuite-81.getty-generator.sh b/test/units/testsuite-81.getty-generator.sh new file mode 100755 index 0000000..103e966 --- /dev/null +++ b/test/units/testsuite-81.getty-generator.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +set -eux +set -o pipefail +# Disable history expansion so we don't have to escape ! in strings below +set +o histexpand + +# shellcheck source=test/units/generator-utils.sh +. "$(dirname "$0")/generator-utils.sh" + +GENERATOR_BIN="/usr/lib/systemd/system-generators/systemd-getty-generator" +OUT_DIR="$(mktemp -d /tmp/getty-generator.XXX)" + +at_exit() { + rm -frv "${OUT_DIR:?}" +} + +trap at_exit EXIT + +test -x "${GENERATOR_BIN:?}" + +if in_container; then + # Do a limited test in a container, as writing to /dev is usually restrited + : "getty-generator: \$container_ttys env (container)" + # In a container we allow only /dev/pts/* ptys + PID1_ENVIRON="container_ttys=tty0 pts/0 /dev/tty0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" + + # console-getty.service is always pulled in in containers + link_endswith "$OUT_DIR/normal/getty.target.wants/console-getty.service" "/lib/systemd/system/console-getty.service" + link_endswith "$OUT_DIR/normal/getty.target.wants/container-getty@0.service" "/lib/systemd/system/container-getty@.service" + test ! -e "$OUT_DIR/normal/getty.target.wants/container-getty@tty0.service" + test ! -h "$OUT_DIR/normal/getty.target.wants/container-getty@tty0.service" + + exit 0 +fi + +DUMMY_ACTIVE_CONSOLES=( + "hvc99" + "xvc99" + "hvsi99" + "sclp_line99" + "ttysclp99" + "3270!tty99" + "dummy99" +) +DUMMY_INACTIVE_CONSOLES=( + "inactive99" + "xvc199" +) +DUMMY_CONSOLES=( + "${DUMMY_ACTIVE_CONSOLES[@]}" + "${DUMMY_INACTIVE_CONSOLES[@]}" +) +# Create a bunch of dummy consoles +for console in "${DUMMY_CONSOLES[@]}"; do + mknod "/dev/$console" c 4 0 +done +# Sneak in one "not-a-tty" console +touch /dev/notatty99 +# Temporarily replace /sys/class/tty/console/active with our list of dummy +# consoles so getty-generator can process them +echo -ne "${DUMMY_ACTIVE_CONSOLES[@]}" /dev/notatty99 >/tmp/dummy-active-consoles +mount -v --bind /tmp/dummy-active-consoles /sys/class/tty/console/active + +: "getty-generator: no arguments" +# Sneak in an invalid value for $SYSTEMD_GETTY_AUTO to test things out +PID1_ENVIRON="SYSTEMD_GETTY_AUTO=foo" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +for console in "${DUMMY_ACTIVE_CONSOLES[@]}"; do + unit="$(systemd-escape --template serial-getty@.service "$console")" + link_endswith "$OUT_DIR/normal/getty.target.wants/$unit" "/lib/systemd/system/serial-getty@.service" +done +for console in "${DUMMY_INACTIVE_CONSOLES[@]}" /dev/notatty99; do + unit="$(systemd-escape --template serial-getty@.service "$console")" + test ! -e "$OUT_DIR/normal/getty.target.wants/$unit" + test ! -h "$OUT_DIR/normal/getty.target.wants/$unit" +done + +: "getty-generator: systemd.getty_auto=0 on kernel cmdline" +SYSTEMD_PROC_CMDLINE="systemd.getty_auto=foo systemd.getty_auto=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +[[ "$(find "$OUT_DIR" ! -type d | wc -l)" -eq 0 ]] + +: "getty-generator: SYSTEMD_GETTY_AUTO=0 in PID1's environment" +PID1_ENVIRON="SYSTEMD_GETTY_AUTO=0" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +[[ "$(find "$OUT_DIR" ! -type d | wc -l)" -eq 0 ]] + +# Cleanup +umount /sys/class/tty/console/active +rm -f "${DUMMY_CONSOLES[@]/#//dev/}" /dev/notatty99 diff --git a/test/units/testsuite-81.run-generator.sh b/test/units/testsuite-81.run-generator.sh new file mode 100755 index 0000000..9bd74ef --- /dev/null +++ b/test/units/testsuite-81.run-generator.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +set -eux +set -o pipefail + +# shellcheck source=test/units/generator-utils.sh +. "$(dirname "$0")/generator-utils.sh" + +GENERATOR_BIN="/usr/lib/systemd/system-generators/systemd-run-generator" +OUT_DIR="$(mktemp -d /tmp/run-generator.XXX)" + +at_exit() { + rm -frv "${OUT_DIR:?}" +} + +trap at_exit EXIT + +test -x "${GENERATOR_BIN:?}" + +check_kernel_cmdline_target() { + local out_dir="${1:?}/normal" + + cat "$out_dir/kernel-command-line.target" + grep -qE "^Requires=kernel-command-line.service$" "$out_dir/kernel-command-line.target" + grep -qE "^After=kernel-command-line.service$" "$out_dir/kernel-command-line.target" + + link_eq "$out_dir/default.target" "kernel-command-line.target" +} + +: "run-generator: empty cmdline" +SYSTEMD_PROC_CMDLINE="" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +[[ "$(find "$OUT_DIR" ! -type d | wc -l)" -eq 0 ]] + +: "run-generator: single command" +CMDLINE="systemd.run='echo hello world'" +SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +check_kernel_cmdline_target "$OUT_DIR" +UNIT="$OUT_DIR/normal/kernel-command-line.service" +cat "$UNIT" +systemd-analyze verify --man=no --recursive-errors=no "$UNIT" +grep -qE "^SuccessAction=exit$" "$UNIT" +grep -qE "^FailureAction=exit$" "$UNIT" +grep -qE "^ExecStart=echo hello world$" "$UNIT" + +: "run-generator: multiple commands + success/failure actions" +ARGS=( + # These should be ignored + "systemd.run" + "systemd.run_success_action" + "systemd.run_failure_action" + + # Set actions which we will overwrite later + "systemd.run_success_action=" + "systemd.run_failure_action=" + + "systemd.run=/bin/false" + "systemd.run=" + "systemd.run=/bin/true" + "systemd.run='echo this is a long string'" + + "systemd.run_success_action=reboot" + "systemd.run_failure_action=poweroff-force" +) +CMDLINE="${ARGS[*]}" +SYSTEMD_PROC_CMDLINE="$CMDLINE" run_and_list "$GENERATOR_BIN" "$OUT_DIR" +check_kernel_cmdline_target "$OUT_DIR" +UNIT="$OUT_DIR/normal/kernel-command-line.service" +cat "$UNIT" +systemd-analyze verify --man=no --recursive-errors=no "$UNIT" +grep -qE "^SuccessAction=reboot$" "$UNIT" +grep -qE "^FailureAction=poweroff-force$" "$UNIT" +grep -qE "^ExecStart=/bin/false$" "$UNIT" +grep -qE "^ExecStart=$" "$UNIT" +grep -qE "^ExecStart=/bin/true$" "$UNIT" +grep -qE "^ExecStart=echo this is a long string$" "$UNIT" diff --git a/test/units/testsuite-81.service b/test/units/testsuite-81.service new file mode 100644 index 0000000..3b697b3 --- /dev/null +++ b/test/units/testsuite-81.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-81-GENERATORS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-81.sh b/test/units/testsuite-81.sh new file mode 100755 index 0000000..9c2a033 --- /dev/null +++ b/test/units/testsuite-81.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok diff --git a/test/units/testsuite-81.system-update-generator.sh b/test/units/testsuite-81.system-update-generator.sh new file mode 100755 index 0000000..8ee1fee --- /dev/null +++ b/test/units/testsuite-81.system-update-generator.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +set -eux +set -o pipefail + +# shellcheck source=test/units/generator-utils.sh +. "$(dirname "$0")/generator-utils.sh" + +GENERATOR_BIN="/usr/lib/systemd/system-generators/systemd-system-update-generator" +OUT_DIR="$(mktemp -d /tmp/system-update-generator-generator.XXX)" + +at_exit() { + rm -frv "${OUT_DIR:?}" /system-update +} + +trap at_exit EXIT + +test -x "${GENERATOR_BIN:?}" + +rm -f /system-update + +: "system-update-generator: no /system-update flag" +run_and_list "$GENERATOR_BIN" "$OUT_DIR" +[[ "$(find "$OUT_DIR" ! -type d | wc -l)" -eq 0 ]] + +: "system-update-generator: with /system-update flag" +touch /system-update +run_and_list "$GENERATOR_BIN" "$OUT_DIR" +link_endswith "$OUT_DIR/early/default.target" "/lib/systemd/system/system-update.target" + +: "system-update-generator: kernel cmdline warnings" +# We should warn if the default target is overridden on the kernel cmdline +# by a runlevel or systemd.unit=, but still generate the symlink +SYSTEMD_PROC_CMDLINE="systemd.unit=foo.bar 3" run_and_list "$GENERATOR_BIN" "$OUT_DIR" |& tee /tmp/system-update-generator.log +link_endswith "$OUT_DIR/early/default.target" "/lib/systemd/system/system-update.target" +grep -qE "Offline system update overridden .* systemd.unit=" /tmp/system-update-generator.log +grep -qE "Offline system update overridden .* runlevel" /tmp/system-update-generator.log diff --git a/test/units/testsuite-82.service b/test/units/testsuite-82.service new file mode 100644 index 0000000..a8fc4f9 --- /dev/null +++ b/test/units/testsuite-82.service @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-82-SOFTREBOOT +DefaultDependencies=no +After=basic.target + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +FileDescriptorStoreMax=3 +NotifyAccess=all diff --git a/test/units/testsuite-82.sh b/test/units/testsuite-82.sh new file mode 100755 index 0000000..b5e6ded --- /dev/null +++ b/test/units/testsuite-82.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +at_exit() { + # Since the soft-reboot drops the enqueued end.service, we won't shutdown + # the test VM if the test fails and have to wait for the watchdog to kill + # us (which may take quite a long time). Let's just forcibly kill the machine + # instead to save CI resources. + if [[ $? -ne 0 ]]; then + echo >&2 "Test failed, shutting down the machine..." + systemctl poweroff -ff + fi +} + +trap at_exit EXIT + +systemd-analyze log-level debug + +export SYSTEMD_LOG_LEVEL=debug + +if [ -f /run/testsuite82.touch3 ]; then + echo "This is the fourth boot!" + systemd-notify --status="Fourth Boot" + + rm /run/testsuite82.touch3 + mount + rmdir /original-root /run/nextroot + + # Check that the fdstore entry still exists + test "$LISTEN_FDS" -eq 3 + read -r x <&5 + test "$x" = "oinkoink" + + # Check that the surviving services are still around + test "$(systemctl show -P ActiveState testsuite-82-survive.service)" = "active" + test "$(systemctl show -P ActiveState testsuite-82-survive-argv.service)" = "active" + test "$(systemctl show -P ActiveState testsuite-82-nosurvive-sigterm.service)" != "active" + test "$(systemctl show -P ActiveState testsuite-82-nosurvive.service)" != "active" + + # Check journals + journalctl -o short-monotonic --no-hostname --grep '(will soft-reboot|KILL|corrupt)' + assert_eq "$(journalctl -q -o short-monotonic -u systemd-journald.service --grep 'corrupt')" "" + + # All succeeded, exit cleanly now + +elif [ -f /run/testsuite82.touch2 ]; then + echo "This is the third boot!" + systemd-notify --status="Third Boot" + + rm /run/testsuite82.touch2 + + # Check that the fdstore entry still exists + test "$LISTEN_FDS" -eq 2 + read -r x <&4 + test "$x" = "miaumiau" + + # Upload another entry + T="/dev/shm/fdstore.$RANDOM" + echo "oinkoink" >"$T" + systemd-notify --fd=3 --pid=parent 3<"$T" + rm "$T" + + # Check that the surviving services are still around + test "$(systemctl show -P ActiveState testsuite-82-survive.service)" = "active" + test "$(systemctl show -P ActiveState testsuite-82-survive-argv.service)" = "active" + test "$(systemctl show -P ActiveState testsuite-82-nosurvive-sigterm.service)" != "active" + test "$(systemctl show -P ActiveState testsuite-82-nosurvive.service)" != "active" + + # Test that we really are in the new overlayfs root fs + read -r x </lower + test "$x" = "miep" + cmp /etc/os-release /run/systemd/propagate/.os-release-stage/os-release + grep -q MARKER=1 /etc/os-release + + # Switch back to the original root, away from the overlayfs + mount --bind /original-root /run/nextroot + mount + + # Restart the unit that is not supposed to survive + systemd-run --collect --service-type=exec --unit=testsuite-82-nosurvive.service sleep infinity + + # Now issue the soft reboot. We should be right back soon. + touch /run/testsuite82.touch3 + systemctl --no-block soft-reboot + + # Now block until the soft-boot killing spree kills us + exec sleep infinity + +elif [ -f /run/testsuite82.touch ]; then + echo "This is the second boot!" + systemd-notify --status="Second Boot" + + # Clean up what we created earlier + rm /run/testsuite82.touch + + # Check that the fdstore entry still exists + test "$LISTEN_FDS" -eq 1 + read -r x <&3 + test "$x" = "wuffwuff" + + # Check that we got a PrepareForShutdownWithMetadata signal with the right type + cat /run/testsuite82.signal + test "$(jq -r '.payload.data[1].type.data' </run/testsuite82.signal)" = "soft-reboot" + + # Upload another entry + T="/dev/shm/fdstore.$RANDOM" + echo "miaumiau" >"$T" + systemd-notify --fd=3 --pid=parent 3<"$T" + rm "$T" + + # Check that the surviving services are still around + test "$(systemctl show -P ActiveState testsuite-82-survive.service)" = "active" + test "$(systemctl show -P ActiveState testsuite-82-survive-argv.service)" = "active" + test "$(systemctl show -P ActiveState testsuite-82-nosurvive-sigterm.service)" != "active" + test "$(systemctl show -P ActiveState testsuite-82-nosurvive.service)" != "active" + + # This time we test the /run/nextroot/ root switching logic. (We synthesize a new rootfs from the old via overlayfs) + mkdir -p /run/nextroot /tmp/nextroot-lower /original-root + mount -t tmpfs tmpfs /tmp/nextroot-lower + echo miep >/tmp/nextroot-lower/lower + + # Copy os-release away, so that we can manipulate it and check that it is updated in the propagate + # directory across soft reboots. Try to cover corner cases by truncating it. + mkdir -p /tmp/nextroot-lower/usr/lib + grep ID /etc/os-release >/tmp/nextroot-lower/usr/lib/os-release + echo MARKER=1 >>/tmp/nextroot-lower/usr/lib/os-release + cmp /etc/os-release /run/systemd/propagate/.os-release-stage/os-release + (! grep -q MARKER=1 /etc/os-release) + + mount -t overlay nextroot /run/nextroot -o lowerdir=/tmp/nextroot-lower:/,ro + + # Bind our current root into the target so that we later can return to it + mount --bind / /run/nextroot/original-root + + # Restart the unit that is not supposed to survive + systemd-run --collect --service-type=exec --unit=testsuite-82-nosurvive.service sleep infinity + + # Now issue the soft reboot. We should be right back soon. Given /run/nextroot exists, we should + # automatically do a softreboot instead of normal reboot. + touch /run/testsuite82.touch2 + systemctl --no-block reboot + + # Now block until the soft-boot killing spree kills us + exec sleep infinity +else + # This is the first boot + systemd-notify --status="First Boot" + + # Let's upload an fd to the fdstore, so that we can verify fdstore passing works correctly + T="/dev/shm/fdstore.$RANDOM" + echo "wuffwuff" >"$T" + systemd-notify --fd=3 --pid=parent 3<"$T" + rm "$T" + + survive_sigterm="/dev/shm/survive-sigterm-$RANDOM.sh" + cat >"$survive_sigterm" <<EOF +#!/bin/bash +trap "" TERM +systemd-notify --ready +rm "$survive_sigterm" +exec sleep infinity +EOF + chmod +x "$survive_sigterm" + + survive_argv="/dev/shm/survive-argv-$RANDOM.sh" + cat >"$survive_argv" <<EOF +#!/bin/bash +systemd-notify --ready +rm "$survive_argv" +exec -a @sleep sleep infinity +EOF + chmod +x "$survive_argv" + # This sets DefaultDependencies=no so that they remain running until the very end, and + # IgnoreOnIsolate=yes so that they aren't stopped via the "testsuite.target" isolation we do on next boot, + # and will be killed by the final sigterm/sigkill spree. + systemd-run --collect --service-type=notify -p DefaultDependencies=no -p IgnoreOnIsolate=yes --unit=testsuite-82-nosurvive-sigterm.service "$survive_sigterm" + systemd-run --collect --service-type=exec -p DefaultDependencies=no -p IgnoreOnIsolate=yes --unit=testsuite-82-nosurvive.service sleep infinity + + # Configure these transient units to survive the soft reboot - they will not conflict with shutdown.target + # and it will be ignored on the isolate that happens in the next boot. The first will use argv[0][0] = + # '@', and the second will use SurviveFinalKillSignal=yes. Both should survive. + systemd-run --service-type=notify --unit=testsuite-82-survive-argv.service \ + --property SurviveFinalKillSignal=no \ + --property IgnoreOnIsolate=yes \ + --property DefaultDependencies=no \ + --property After=basic.target \ + --property "Conflicts=reboot.target kexec.target poweroff.target halt.target emergency.target rescue.target" \ + --property "Before=reboot.target kexec.target poweroff.target halt.target emergency.target rescue.target" \ + "$survive_argv" + systemd-run --service-type=exec --unit=testsuite-82-survive.service \ + --property SurviveFinalKillSignal=yes \ + --property IgnoreOnIsolate=yes \ + --property DefaultDependencies=no \ + --property After=basic.target \ + --property "Conflicts=reboot.target kexec.target poweroff.target halt.target emergency.target rescue.target" \ + --property "Before=reboot.target kexec.target poweroff.target halt.target emergency.target rescue.target" \ + sleep infinity + + # Check that we can set up an inhibitor, and that busctl monitor sees the + # PrepareForShutdownWithMetadata signal and that it says 'soft-reboot'. + systemd-run --unit busctl.service --service-type=exec --property StandardOutput=file:/run/testsuite82.signal \ + busctl monitor --json=pretty --match 'sender=org.freedesktop.login1,path=/org/freedesktop/login1,interface=org.freedesktop.login1.Manager,member=PrepareForShutdownWithMetadata,type=signal' + systemd-run --unit inhibit.service --service-type=exec \ + systemd-inhibit --what=shutdown --who=test --why=test --mode=delay \ + sleep infinity + + # Now issue the soft reboot. We should be right back soon. + touch /run/testsuite82.touch + systemctl --no-block --check-inhibitors=yes soft-reboot + + # Now block until the soft-boot killing spree kills us + exec sleep infinity +fi + +systemd-analyze log-level info + +touch /testok +systemctl --no-block poweroff diff --git a/test/units/testsuite-83.service b/test/units/testsuite-83.service new file mode 100644 index 0000000..55ebb45 --- /dev/null +++ b/test/units/testsuite-83.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-83-BTRFS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-83.sh b/test/units/testsuite-83.sh new file mode 100755 index 0000000..a722c79 --- /dev/null +++ b/test/units/testsuite-83.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +TEST_BTRFS_OFFSET=/usr/lib/systemd/tests/unit-tests/manual/test-btrfs-physical-offset + +SWAPFILE=/var/tmp/swapfile + +btrfs filesystem mkswapfile -s 10m "$SWAPFILE" +sync -f "$SWAPFILE" + +offset_btrfs_progs="$(btrfs inspect-internal map-swapfile -r "$SWAPFILE")" +echo "btrfs-progs: $offset_btrfs_progs" + +offset_btrfs_util="$("$TEST_BTRFS_OFFSET" "$SWAPFILE")" +echo "btrfs-util: $offset_btrfs_util" + +(( offset_btrfs_progs == offset_btrfs_util )) + +rm -f "$SWAPFILE" + +/usr/lib/systemd/tests/unit-tests/manual/test-btrfs + +touch /testok diff --git a/test/units/testsuite-84.service b/test/units/testsuite-84.service new file mode 100644 index 0000000..2c25770 --- /dev/null +++ b/test/units/testsuite-84.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-84-STORAGETM +After=multi-user.target + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-84.sh b/test/units/testsuite-84.sh new file mode 100755 index 0000000..eae87d5 --- /dev/null +++ b/test/units/testsuite-84.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +modprobe -v nvmet-tcp +modprobe -v nvme-tcp + +systemctl start sys-kernel-config.mount + +dd if=/dev/urandom of=/var/tmp/storagetm.test bs=1024 count=10240 + +NVME_UUID="$(cat /proc/sys/kernel/random/uuid)" +systemd-run -u teststoragetm.service -p Type=notify -p "Environment=SYSTEMD_NVME_UUID=${NVME_UUID:?}" /usr/lib/systemd/systemd-storagetm /var/tmp/storagetm.test --nqn=quux +NVME_DEVICE="/dev/disk/by-id/nvme-uuid.${NVME_UUID:?}" + +nvme connect-all -t tcp -a 127.0.0.1 -s 16858 --hostid="$(cat /proc/sys/kernel/random/uuid)" +udevadm wait --settle "$NVME_DEVICE" + +dd if="$NVME_DEVICE" bs=1024 | cmp /var/tmp/storagetm.test - + +nvme disconnect-all +systemctl stop teststoragetm.service +rm /var/tmp/storagetm.test + +touch /testok diff --git a/test/units/testsuite.target b/test/units/testsuite.target new file mode 100644 index 0000000..6bcbfec --- /dev/null +++ b/test/units/testsuite.target @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Testsuite target +Requires=multi-user.target +After=multi-user.target +Conflicts=rescue.target +AllowIsolate=yes diff --git a/test/units/timers.target b/test/units/timers.target new file mode 100644 index 0000000..99f82e3 --- /dev/null +++ b/test/units/timers.target @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Timers +Documentation=man:systemd.special(7) + +DefaultDependencies=no +Conflicts=shutdown.target diff --git a/test/units/unit-.service.d/10-override.conf b/test/units/unit-.service.d/10-override.conf new file mode 100644 index 0000000..1bc5e1c --- /dev/null +++ b/test/units/unit-.service.d/10-override.conf @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=override0 diff --git a/test/units/unit-with-.service.d/20-override.conf b/test/units/unit-with-.service.d/20-override.conf new file mode 100644 index 0000000..17fe084 --- /dev/null +++ b/test/units/unit-with-.service.d/20-override.conf @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Documentation=man:override1 diff --git a/test/units/unit-with-multiple-.service.d/20-override.conf b/test/units/unit-with-multiple-.service.d/20-override.conf new file mode 100644 index 0000000..5b48784 --- /dev/null +++ b/test/units/unit-with-multiple-.service.d/20-override.conf @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Documentation=man:override2 diff --git a/test/units/unit-with-multiple-.service.d/30-override.conf b/test/units/unit-with-multiple-.service.d/30-override.conf new file mode 100644 index 0000000..4d3423a --- /dev/null +++ b/test/units/unit-with-multiple-.service.d/30-override.conf @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Documentation=man:override3 diff --git a/test/units/unit-with-multiple-dashes.service b/test/units/unit-with-multiple-dashes.service new file mode 100644 index 0000000..4aca904 --- /dev/null +++ b/test/units/unit-with-multiple-dashes.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=A unit with multiple dashes +Documentation=man:test + +[Service] +ExecStart=/bin/true diff --git a/test/units/unit-with-multiple-dashes.service.d/10-override.conf b/test/units/unit-with-multiple-dashes.service.d/10-override.conf new file mode 100644 index 0000000..e249b20 --- /dev/null +++ b/test/units/unit-with-multiple-dashes.service.d/10-override.conf @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=override4 diff --git a/test/units/util.sh b/test/units/util.sh new file mode 100755 index 0000000..b5ed732 --- /dev/null +++ b/test/units/util.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Utility functions for shell tests + +# shellcheck disable=SC2034 +[[ -e /var/tmp/.systemd_reboot_count ]] && REBOOT_COUNT="$(</var/tmp/.systemd_reboot_count)" || REBOOT_COUNT=0 + +assert_true() {( + set +ex + + local rc + + "$@" + rc=$? + if [[ $rc -ne 0 ]]; then + echo "FAIL: command '$*' failed with exit code $rc" >&2 + exit 1 + fi +)} + +assert_eq() {( + set +ex + + if [[ "${1?}" != "${2?}" ]]; then + echo "FAIL: expected: '$2' actual: '$1'" >&2 + exit 1 + fi +)} + +assert_in() {( + set +ex + + if ! [[ "${2?}" =~ ${1?} ]]; then + echo "FAIL: '$1' not found in:" >&2 + echo "$2" >&2 + exit 1 + fi +)} + +assert_not_in() {( + set +ex + + if [[ "${2?}" =~ ${1?} ]]; then + echo "FAIL: '$1' found in:" >&2 + echo "$2" >&2 + exit 1 + fi +)} + +assert_rc() {( + set +ex + + local rc exp="${1?}" + + shift + "$@" + rc=$? + assert_eq "$rc" "$exp" +)} + +assert_not_reached() { + echo >&2 "Code should not be reached at ${BASH_SOURCE[1]}:${BASH_LINENO[1]}, function ${FUNCNAME[1]}()" + exit 1 +} + +run_and_grep() {( + set +ex + + local expression + local log ec + local exp_ec=0 + + # Invert the grep condition - i.e. check if the expression is _not_ in command's output + if [[ "${1:?}" == "-n" ]]; then + exp_ec=1 + shift + fi + + expression="${1:?}" + shift + + if [[ $# -eq 0 ]]; then + echo >&2 "FAIL: Not enough arguments for ${FUNCNAME[0]}()" + return 1 + fi + + log="$(mktemp)" + if ! "$@" |& tee "${log:?}"; then + echo >&2 "FAIL: Command '$*' failed" + return 1 + fi + + grep -qE "$expression" "$log" && ec=0 || ec=$? + if [[ "$exp_ec" -eq 0 && "$ec" -ne 0 ]]; then + echo >&2 "FAIL: Expression '$expression' not found in the output of '$*'" + return 1 + elif [[ "$exp_ec" -ne 0 && "$ec" -eq 0 ]]; then + echo >&2 "FAIL: Expression '$expression' found in the output of '$*'" + return 1 + fi + + rm -f "$log" +)} + +get_cgroup_hierarchy() { + case "$(stat -c '%T' -f /sys/fs/cgroup)" in + cgroup2fs) + echo "unified" + ;; + tmpfs) + if [[ -d /sys/fs/cgroup/unified && "$(stat -c '%T' -f /sys/fs/cgroup/unified)" == cgroup2fs ]]; then + echo "hybrid" + else + echo "legacy" + fi + ;; + *) + echo >&2 "Failed to determine host's cgroup hierarchy" + exit 1 + esac +} + +runas() { + local userid="${1:?}" + shift + XDG_RUNTIME_DIR=/run/user/"$(id -u "$userid")" setpriv --reuid="$userid" --init-groups "$@" +} + +coverage_create_nspawn_dropin() { + # If we're collecting coverage, bind mount the $BUILD_DIR into the nspawn + # container so gcov can update the counters. This is mostly for standalone + # containers, as machinectl stuff is handled by overriding the systemd-nspawn@.service + # (see test/test-functions:install_systemd()) + local root="${1:?}" + local container + + if [[ -z "${COVERAGE_BUILD_DIR:-}" ]]; then + return 0 + fi + + container="$(basename "$root")" + mkdir -p "/run/systemd/nspawn" + echo -ne "[Files]\nBind=$COVERAGE_BUILD_DIR\n" >"/run/systemd/nspawn/${container:?}.nspawn" +} + +create_dummy_container() { + local root="${1:?}" + + if [[ ! -d /testsuite-13-container-template ]]; then + echo >&2 "Missing container template, probably not running in TEST-13-NSPAWN?" + exit 1 + fi + + mkdir -p "$root" + cp -a /testsuite-13-container-template/* "$root" + coverage_create_nspawn_dropin "$root" +} + +# Bump the reboot counter and call systemctl with the given arguments +systemctl_final() { + local counter + + if [[ $# -eq 0 ]]; then + echo >&2 "Missing arguments" + exit 1 + fi + + [[ -e /var/tmp/.systemd_reboot_count ]] && counter="$(</var/tmp/.systemd_reboot_count)" || counter=0 + echo "$((counter + 1))" >/var/tmp/.systemd_reboot_count + + systemctl "$@" +} + +cgroupfs_supports_user_xattrs() { + local xattr + + xattr="user.supported_$RANDOM" + # shellcheck disable=SC2064 + trap "setfattr --remove=$xattr /sys/fs/cgroup || :" RETURN + + setfattr --name="$xattr" --value=254 /sys/fs/cgroup + [[ "$(getfattr --name="$xattr" --absolute-names --only-values /sys/fs/cgroup)" -eq 254 ]] +} + +tpm_has_pcr() { + local algorithm="${1:?}" + local pcr="${2:?}" + + [[ -f "/sys/class/tpm/tpm0/pcr-$algorithm/$pcr" ]] +} + +openssl_supports_kdf() { + local kdf="${1:?}" + + # The arguments will need to be adjusted to make this work for other KDFs than SSKDF, + # but let's do that when/if the need arises + openssl kdf -keylen 16 -kdfopt digest:SHA2-256 -kdfopt key:foo -out /dev/null "$kdf" +} + +kernel_supports_lsm() { + local lsm="${1:?}" + local items item + + if [[ ! -e /sys/kernel/security/lsm ]]; then + echo "/sys/kernel/security/lsm doesn't exist, assuming $lsm is not supported" + return 1 + fi + + mapfile -t -d, items </sys/kernel/security/lsm + for item in "${items[@]}"; do + if [[ "$item" == "$lsm" ]]; then + return 0 + fi + done + + return 1 +} |