summaryrefslogtreecommitdiffstats
path: root/test/units
diff options
context:
space:
mode:
Diffstat (limited to 'test/units')
-rw-r--r--test/units/a-conj.service9
-rw-r--r--test/units/a.service8
-rw-r--r--test/units/autorelabel.service19
-rw-r--r--test/units/b.service7
-rw-r--r--test/units/basic.target22
-rw-r--r--test/units/c.service7
-rw-r--r--test/units/d.service9
-rw-r--r--test/units/daughter.service9
-rwxr-xr-xtest/units/delegated_cgroup_filtering_payload.sh12
-rwxr-xr-xtest/units/delegated_cgroup_filtering_payload_child.sh11
-rw-r--r--test/units/dml-discard-empty.service8
-rw-r--r--test/units/dml-discard-set-ml.service9
-rw-r--r--test/units/dml-discard.slice6
-rw-r--r--test/units/dml-override-empty.service8
-rw-r--r--test/units/dml-override.slice6
-rw-r--r--test/units/dml-passthrough-empty.service8
-rw-r--r--test/units/dml-passthrough-set-dml.service9
-rw-r--r--test/units/dml-passthrough-set-ml.service9
-rw-r--r--test/units/dml-passthrough.slice6
-rw-r--r--test/units/dml.slice6
-rw-r--r--test/units/e.service9
-rw-r--r--test/units/end.service11
-rwxr-xr-xtest/units/end.sh13
-rw-r--r--test/units/f.service6
-rw-r--r--test/units/g.service7
-rwxr-xr-xtest/units/generator-utils.sh78
-rw-r--r--test/units/grandchild.service8
-rw-r--r--test/units/h.service7
-rw-r--r--test/units/i.service9
-rw-r--r--test/units/loopy.service3
-rw-r--r--test/units/loopy.service.d/compat.conf6
-rw-r--r--test/units/loopy2.service3
-rw-r--r--test/units/loopy3.service6
-rw-r--r--test/units/loopy4.service6
-rw-r--r--test/units/nomem.slice6
-rw-r--r--test/units/nomemleaf.service10
-rw-r--r--test/units/parent-deep.slice6
-rw-r--r--test/units/parent.slice6
-rw-r--r--test/units/sched_idle_bad.service7
-rw-r--r--test/units/sched_idle_ok.service7
-rw-r--r--test/units/sched_rr_bad.service9
-rw-r--r--test/units/sched_rr_change.service10
-rw-r--r--test/units/sched_rr_ok.service7
-rw-r--r--test/units/shutdown.target14
-rw-r--r--test/units/sockets.target12
-rw-r--r--test/units/son.service9
-rw-r--r--test/units/success-failure-test-failure.service3
-rw-r--r--test/units/success-failure-test-success.service3
-rw-r--r--test/units/success-failure-test.service9
-rw-r--r--test/units/sysinit.target15
-rw-r--r--test/units/test-control.sh166
-rw-r--r--test/units/testsuite-01.service10
-rwxr-xr-xtest/units/testsuite-01.sh61
-rw-r--r--test/units/testsuite-02.service8
-rwxr-xr-xtest/units/testsuite-02.sh113
-rw-r--r--test/units/testsuite-03.service9
-rwxr-xr-xtest/units/testsuite-03.sh169
-rwxr-xr-xtest/units/testsuite-04.LogFilterPatterns.sh86
-rwxr-xr-xtest/units/testsuite-04.SYSTEMD_JOURNAL_COMPRESS.sh42
-rwxr-xr-xtest/units/testsuite-04.bsod.sh103
-rwxr-xr-xtest/units/testsuite-04.corrupted-journals.sh48
-rwxr-xr-xtest/units/testsuite-04.fss.sh46
-rwxr-xr-xtest/units/testsuite-04.journal-append.sh46
-rwxr-xr-xtest/units/testsuite-04.journal-gatewayd.sh119
-rwxr-xr-xtest/units/testsuite-04.journal-remote.sh230
-rwxr-xr-xtest/units/testsuite-04.journal.sh271
-rw-r--r--test/units/testsuite-04.service8
-rwxr-xr-xtest/units/testsuite-04.sh11
-rw-r--r--test/units/testsuite-05.service8
-rwxr-xr-xtest/units/testsuite-05.sh27
-rw-r--r--test/units/testsuite-06.service8
-rwxr-xr-xtest/units/testsuite-06.sh43
-rwxr-xr-xtest/units/testsuite-07.exec-context.sh375
-rwxr-xr-xtest/units/testsuite-07.issue-14566.sh29
-rwxr-xr-xtest/units/testsuite-07.issue-16115.sh16
-rwxr-xr-xtest/units/testsuite-07.issue-1981.sh47
-rwxr-xr-xtest/units/testsuite-07.issue-2467.sh17
-rwxr-xr-xtest/units/testsuite-07.issue-27953.sh11
-rwxr-xr-xtest/units/testsuite-07.issue-30412.sh32
-rwxr-xr-xtest/units/testsuite-07.issue-3166.sh16
-rwxr-xr-xtest/units/testsuite-07.issue-3171.sh50
-rwxr-xr-xtest/units/testsuite-07.main-PID-change.sh172
-rwxr-xr-xtest/units/testsuite-07.mount-invalid-chars.sh70
-rwxr-xr-xtest/units/testsuite-07.poll-limit.sh48
-rwxr-xr-xtest/units/testsuite-07.private-network.sh7
-rw-r--r--test/units/testsuite-07.service13
-rwxr-xr-xtest/units/testsuite-07.sh15
-rw-r--r--test/units/testsuite-08.service9
-rwxr-xr-xtest/units/testsuite-08.sh30
-rwxr-xr-xtest/units/testsuite-09.journal.sh72
-rw-r--r--test/units/testsuite-09.service9
-rwxr-xr-xtest/units/testsuite-09.sh25
-rwxr-xr-xtest/units/testsuite-13.machinectl.sh218
-rwxr-xr-xtest/units/testsuite-13.nspawn-oci.sh467
-rwxr-xr-xtest/units/testsuite-13.nspawn.sh884
-rwxr-xr-xtest/units/testsuite-13.nss-mymachines.sh135
-rw-r--r--test/units/testsuite-13.service8
-rwxr-xr-xtest/units/testsuite-13.sh11
-rw-r--r--test/units/testsuite-15.service8
-rwxr-xr-xtest/units/testsuite-15.sh711
-rw-r--r--test/units/testsuite-16.service20
-rwxr-xr-xtest/units/testsuite-16.sh119
-rwxr-xr-xtest/units/testsuite-17.00.sh57
-rwxr-xr-xtest/units/testsuite-17.01.sh75
-rwxr-xr-xtest/units/testsuite-17.02.sh182
-rwxr-xr-xtest/units/testsuite-17.03.sh75
-rwxr-xr-xtest/units/testsuite-17.04.sh49
-rwxr-xr-xtest/units/testsuite-17.05.sh23
-rwxr-xr-xtest/units/testsuite-17.06.sh69
-rwxr-xr-xtest/units/testsuite-17.07.sh205
-rwxr-xr-xtest/units/testsuite-17.08.sh72
-rwxr-xr-xtest/units/testsuite-17.09.sh70
-rwxr-xr-xtest/units/testsuite-17.10.sh254
-rwxr-xr-xtest/units/testsuite-17.11.sh447
-rwxr-xr-xtest/units/testsuite-17.12.sh86
-rwxr-xr-xtest/units/testsuite-17.13.sh89
-rw-r--r--test/units/testsuite-17.service8
-rwxr-xr-xtest/units/testsuite-17.sh13
-rw-r--r--test/units/testsuite-18.service8
-rwxr-xr-xtest/units/testsuite-18.sh17
-rwxr-xr-xtest/units/testsuite-19.ExitType-cgroup.sh102
-rwxr-xr-xtest/units/testsuite-19.cleanup-slice.sh49
-rwxr-xr-xtest/units/testsuite-19.delegate.sh115
-rw-r--r--test/units/testsuite-19.service8
-rwxr-xr-xtest/units/testsuite-19.sh11
-rw-r--r--test/units/testsuite-21.service10
-rwxr-xr-xtest/units/testsuite-21.sh110
-rwxr-xr-xtest/units/testsuite-22.01.sh13
-rwxr-xr-xtest/units/testsuite-22.02.sh167
-rwxr-xr-xtest/units/testsuite-22.03.sh246
-rwxr-xr-xtest/units/testsuite-22.04.sh43
-rwxr-xr-xtest/units/testsuite-22.05.sh45
-rwxr-xr-xtest/units/testsuite-22.06.sh38
-rwxr-xr-xtest/units/testsuite-22.07.sh30
-rwxr-xr-xtest/units/testsuite-22.08.sh32
-rwxr-xr-xtest/units/testsuite-22.09.sh59
-rwxr-xr-xtest/units/testsuite-22.10.sh28
-rwxr-xr-xtest/units/testsuite-22.11.sh141
-rwxr-xr-xtest/units/testsuite-22.12.sh196
-rwxr-xr-xtest/units/testsuite-22.13.sh75
-rwxr-xr-xtest/units/testsuite-22.14.sh37
-rwxr-xr-xtest/units/testsuite-22.15.sh32
-rwxr-xr-xtest/units/testsuite-22.16.sh36
-rwxr-xr-xtest/units/testsuite-22.17.sh15
-rw-r--r--test/units/testsuite-22.service11
-rwxr-xr-xtest/units/testsuite-22.sh11
-rwxr-xr-xtest/units/testsuite-23-short-lived.sh18
-rwxr-xr-xtest/units/testsuite-23.ExecReload.sh61
-rwxr-xr-xtest/units/testsuite-23.ExecStopPost.sh104
-rwxr-xr-xtest/units/testsuite-23.JoinsNamespaceOf.sh31
-rwxr-xr-xtest/units/testsuite-23.RuntimeDirectoryPreserve.sh26
-rwxr-xr-xtest/units/testsuite-23.StandardOutput.sh60
-rwxr-xr-xtest/units/testsuite-23.Upholds.sh99
-rwxr-xr-xtest/units/testsuite-23.clean-unit.sh329
-rwxr-xr-xtest/units/testsuite-23.exec-command-ex.sh44
-rwxr-xr-xtest/units/testsuite-23.oneshot-restart.sh52
-rwxr-xr-xtest/units/testsuite-23.percentj-wantedby.sh15
-rwxr-xr-xtest/units/testsuite-23.runtime-bind-paths.sh43
-rw-r--r--test/units/testsuite-23.service8
-rwxr-xr-xtest/units/testsuite-23.sh12
-rwxr-xr-xtest/units/testsuite-23.start-stop-no-reload.sh93
-rwxr-xr-xtest/units/testsuite-23.statedir.sh60
-rwxr-xr-xtest/units/testsuite-23.success-failure.sh49
-rwxr-xr-xtest/units/testsuite-23.type-exec.sh63
-rwxr-xr-xtest/units/testsuite-23.utmp.sh22
-rwxr-xr-xtest/units/testsuite-23.whoami.sh15
-rw-r--r--test/units/testsuite-24.service9
-rwxr-xr-xtest/units/testsuite-24.sh216
-rw-r--r--test/units/testsuite-25.service8
-rwxr-xr-xtest/units/testsuite-25.sh143
-rw-r--r--test/units/testsuite-26.service8
-rwxr-xr-xtest/units/testsuite-26.sh465
-rw-r--r--test/units/testsuite-29.service8
-rwxr-xr-xtest/units/testsuite-29.sh280
-rw-r--r--test/units/testsuite-30.service8
-rwxr-xr-xtest/units/testsuite-30.sh29
-rw-r--r--test/units/testsuite-31.service8
-rwxr-xr-xtest/units/testsuite-31.sh10
-rw-r--r--test/units/testsuite-32.service9
-rwxr-xr-xtest/units/testsuite-32.sh36
-rw-r--r--test/units/testsuite-34.service8
-rwxr-xr-xtest/units/testsuite-34.sh160
-rw-r--r--test/units/testsuite-35.service8
-rwxr-xr-xtest/units/testsuite-35.sh660
-rw-r--r--test/units/testsuite-36.service8
-rwxr-xr-xtest/units/testsuite-36.sh352
-rw-r--r--test/units/testsuite-38-sleep.service3
-rw-r--r--test/units/testsuite-38.service7
-rwxr-xr-xtest/units/testsuite-38.sh301
-rw-r--r--test/units/testsuite-43.service10
-rwxr-xr-xtest/units/testsuite-43.sh143
-rw-r--r--test/units/testsuite-44.service12
-rwxr-xr-xtest/units/testsuite-44.sh18
-rw-r--r--test/units/testsuite-45.service8
-rwxr-xr-xtest/units/testsuite-45.sh412
-rw-r--r--test/units/testsuite-46.service13
-rwxr-xr-xtest/units/testsuite-46.sh319
-rw-r--r--test/units/testsuite-50.service8
-rwxr-xr-xtest/units/testsuite-50.sh718
-rw-r--r--test/units/testsuite-52.service7
-rwxr-xr-xtest/units/testsuite-52.sh11
-rw-r--r--test/units/testsuite-53.service8
-rwxr-xr-xtest/units/testsuite-53.sh31
-rw-r--r--test/units/testsuite-54.service8
-rwxr-xr-xtest/units/testsuite-54.sh319
-rw-r--r--test/units/testsuite-55-testbloat.service10
-rw-r--r--test/units/testsuite-55-testchill.service8
-rw-r--r--test/units/testsuite-55-testmunch.service8
-rw-r--r--test/units/testsuite-55-workload.slice11
-rw-r--r--test/units/testsuite-55.service10
-rwxr-xr-xtest/units/testsuite-55.sh182
-rw-r--r--test/units/testsuite-58.service7
-rwxr-xr-xtest/units/testsuite-58.sh1307
-rw-r--r--test/units/testsuite-59.service7
-rwxr-xr-xtest/units/testsuite-59.sh160
-rw-r--r--test/units/testsuite-60.service8
-rwxr-xr-xtest/units/testsuite-60.sh308
-rw-r--r--test/units/testsuite-62-1.service9
-rw-r--r--test/units/testsuite-62-2.service10
-rw-r--r--test/units/testsuite-62-3.service10
-rw-r--r--test/units/testsuite-62-4.service10
-rw-r--r--test/units/testsuite-62-5.service11
-rw-r--r--test/units/testsuite-62.service8
-rwxr-xr-xtest/units/testsuite-62.sh63
-rw-r--r--test/units/testsuite-63.service8
-rwxr-xr-xtest/units/testsuite-63.sh125
-rw-r--r--test/units/testsuite-64.service8
-rwxr-xr-xtest/units/testsuite-64.sh1192
-rw-r--r--test/units/testsuite-65.service8
-rwxr-xr-xtest/units/testsuite-65.sh909
-rw-r--r--test/units/testsuite-66-deviceisolation.service10
-rw-r--r--test/units/testsuite-66.service8
-rwxr-xr-xtest/units/testsuite-66.sh24
-rw-r--r--test/units/testsuite-67.service9
-rwxr-xr-xtest/units/testsuite-67.sh121
-rw-r--r--test/units/testsuite-68.service7
-rwxr-xr-xtest/units/testsuite-68.sh216
-rw-r--r--test/units/testsuite-69.service7
-rwxr-xr-xtest/units/testsuite-70.creds.sh16
-rwxr-xr-xtest/units/testsuite-70.cryptenroll.sh84
-rwxr-xr-xtest/units/testsuite-70.cryptsetup.sh226
-rwxr-xr-xtest/units/testsuite-70.measure.sh130
-rwxr-xr-xtest/units/testsuite-70.pcrextend.sh124
-rwxr-xr-xtest/units/testsuite-70.pcrlock.sh146
-rw-r--r--test/units/testsuite-70.service7
-rwxr-xr-xtest/units/testsuite-70.sh11
-rwxr-xr-xtest/units/testsuite-70.tpm2-setup.sh27
-rw-r--r--test/units/testsuite-71.service8
-rwxr-xr-xtest/units/testsuite-71.sh228
-rw-r--r--test/units/testsuite-72.service8
-rwxr-xr-xtest/units/testsuite-72.sh278
-rw-r--r--test/units/testsuite-73.service8
-rwxr-xr-xtest/units/testsuite-73.sh693
-rwxr-xr-xtest/units/testsuite-74.battery-check.sh9
-rwxr-xr-xtest/units/testsuite-74.bootctl.sh266
-rwxr-xr-xtest/units/testsuite-74.busctl.sh110
-rwxr-xr-xtest/units/testsuite-74.cgls.sh27
-rwxr-xr-xtest/units/testsuite-74.cgtop.sh32
-rwxr-xr-xtest/units/testsuite-74.coredump.sh221
-rwxr-xr-xtest/units/testsuite-74.delta.sh59
-rwxr-xr-xtest/units/testsuite-74.escape.sh108
-rwxr-xr-xtest/units/testsuite-74.firstboot.sh197
-rwxr-xr-xtest/units/testsuite-74.id128.sh50
-rwxr-xr-xtest/units/testsuite-74.machine-id-setup.sh77
-rwxr-xr-xtest/units/testsuite-74.modules-load.sh88
-rwxr-xr-xtest/units/testsuite-74.mount.sh151
-rwxr-xr-xtest/units/testsuite-74.networkctl.sh86
-rwxr-xr-xtest/units/testsuite-74.path.sh89
-rwxr-xr-xtest/units/testsuite-74.pstore.sh258
-rwxr-xr-xtest/units/testsuite-74.run.sh236
-rw-r--r--test/units/testsuite-74.service8
-rwxr-xr-xtest/units/testsuite-74.sh11
-rwxr-xr-xtest/units/testsuite-74.varlinkctl.sh89
-rw-r--r--test/units/testsuite-75.service8
-rwxr-xr-xtest/units/testsuite-75.sh729
-rw-r--r--test/units/testsuite-76.service8
-rwxr-xr-xtest/units/testsuite-76.sh39
-rwxr-xr-xtest/units/testsuite-77-client.sh14
-rwxr-xr-xtest/units/testsuite-77-run.sh14
-rw-r--r--test/units/testsuite-77-server.socket6
-rw-r--r--test/units/testsuite-77-server@.service7
-rw-r--r--test/units/testsuite-77.service10
-rwxr-xr-xtest/units/testsuite-77.sh38
-rw-r--r--test/units/testsuite-78.service7
-rwxr-xr-xtest/units/testsuite-78.sh35
-rw-r--r--test/units/testsuite-79.service8
-rwxr-xr-xtest/units/testsuite-79.sh58
-rw-r--r--test/units/testsuite-80.service8
-rwxr-xr-xtest/units/testsuite-80.sh126
-rwxr-xr-xtest/units/testsuite-81.debug-generator.sh105
-rwxr-xr-xtest/units/testsuite-81.environment-d-generator.sh80
-rwxr-xr-xtest/units/testsuite-81.fstab-generator.sh406
-rwxr-xr-xtest/units/testsuite-81.getty-generator.sh89
-rwxr-xr-xtest/units/testsuite-81.run-generator.sh76
-rw-r--r--test/units/testsuite-81.service8
-rwxr-xr-xtest/units/testsuite-81.sh11
-rwxr-xr-xtest/units/testsuite-81.system-update-generator.sh38
-rw-r--r--test/units/testsuite-82.service11
-rwxr-xr-xtest/units/testsuite-82.sh223
-rw-r--r--test/units/testsuite-83.service8
-rwxr-xr-xtest/units/testsuite-83.sh25
-rw-r--r--test/units/testsuite-84.service9
-rwxr-xr-xtest/units/testsuite-84.sh26
-rw-r--r--test/units/testsuite.target7
-rw-r--r--test/units/timers.target15
-rw-r--r--test/units/unit-.service.d/10-override.conf3
-rw-r--r--test/units/unit-with-.service.d/20-override.conf3
-rw-r--r--test/units/unit-with-multiple-.service.d/20-override.conf3
-rw-r--r--test/units/unit-with-multiple-.service.d/30-override.conf3
-rw-r--r--test/units/unit-with-multiple-dashes.service7
-rw-r--r--test/units/unit-with-multiple-dashes.service.d/10-override.conf3
-rwxr-xr-xtest/units/util.sh218
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
+}