]> git.proxmox.com Git - systemd.git/blobdiff - test/test-functions
New upstream version 252.5
[systemd.git] / test / test-functions
index c6b8d4cd59942744ddf492ee3e1e8c06544c72fa..ae0a99333749a146d4a1070456f4eed448aaa3d6 100644 (file)
@@ -1,6 +1,8 @@
 #!/usr/bin/env bash
-# shellcheck disable=SC2031
 # -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# shellcheck disable=SC2030,SC2031
 # ex: ts=8 sw=4 sts=4 et filetype=sh tw=180
 # Note: the shellcheck line above disables warning for variables which were
 #       modified in a subshell. In our case this behavior is expected, but
@@ -22,13 +24,12 @@ source "$os_release"
 [[ "$ID" = "arch" || " $ID_LIKE " = *" arch "* ]] && LOOKS_LIKE_ARCH=yes || LOOKS_LIKE_ARCH=""
 [[ " $ID_LIKE " = *" suse "* ]] && LOOKS_LIKE_SUSE=yes || LOOKS_LIKE_SUSE=""
 KERNEL_VER="${KERNEL_VER-$(uname -r)}"
-QEMU_TIMEOUT="${QEMU_TIMEOUT:-infinity}"
-NSPAWN_TIMEOUT="${NSPAWN_TIMEOUT:-infinity}"
+QEMU_TIMEOUT="${QEMU_TIMEOUT:-1800}"
+NSPAWN_TIMEOUT="${NSPAWN_TIMEOUT:-1800}"
 TIMED_OUT=  # will be 1 after run_* if *_TIMEOUT is set and test timed out
 [[ "$LOOKS_LIKE_SUSE" ]] && FSTYPE="${FSTYPE:-btrfs}" || FSTYPE="${FSTYPE:-ext4}"
 UNIFIED_CGROUP_HIERARCHY="${UNIFIED_CGROUP_HIERARCHY:-default}"
 EFI_MOUNT="${EFI_MOUNT:-$(bootctl -x 2>/dev/null || echo /boot)}"
-QEMU_MEM="${QEMU_MEM:-512M}"
 # Note that defining a different IMAGE_NAME in a test setup script will only result
 # in default.img being copied and renamed. It can then be extended by defining
 # a test_append_files() function. The $1 parameter will be the root directory.
@@ -38,6 +39,7 @@ IMAGE_NAME=${IMAGE_NAME:-default}
 STRIP_BINARIES="${STRIP_BINARIES:-yes}"
 TEST_REQUIRE_INSTALL_TESTS="${TEST_REQUIRE_INSTALL_TESTS:-1}"
 TEST_PARALLELIZE="${TEST_PARALLELIZE:-0}"
+TEST_SUPPORTING_SERVICES_SHOULD_BE_MASKED="${TEST_SUPPORTING_SERVICES_SHOULD_BE_MASKED:-1}"
 LOOPDEV=
 
 # Simple wrapper to unify boolean checks.
@@ -58,12 +60,39 @@ get_bool() {
     fi
 }
 
-# Decide if we can (and want to) run QEMU with KVM acceleration.
+# Since in Bash we can have only one handler per signal, let's overcome this
+# limitation by having one global handler for the EXIT signal which executes
+# all registered handlers
+_AT_EXIT_HANDLERS=()
+_at_exit() {
+    set +e
+
+    # Run the EXIT handlers in reverse order
+    for ((i = ${#_AT_EXIT_HANDLERS[@]} - 1; i >= 0; i--)); do
+        ddebug "Running EXIT handler '${_AT_EXIT_HANDLERS[$i]}'"
+        "${_AT_EXIT_HANDLERS[$i]}"
+    done
+}
+
+trap _at_exit EXIT
+
+add_at_exit_handler() {
+    local handler="${1?}"
+
+    if [[ "$(type -t "$handler")" != "function" ]]; then
+        dfatal "'$handler' is not a function"
+        exit 1
+    fi
+
+    _AT_EXIT_HANDLERS+=("$handler")
+}
+
+# Decide if we can (and want to) run qemu with KVM acceleration.
 # Check if nested KVM is explicitly enabled (TEST_NESTED_KVM). If not,
 # check if it's not explicitly disabled (TEST_NO_KVM) and we're not already
 # running under KVM. If these conditions are met, enable KVM (and possibly
 # nested KVM), otherwise disable it.
-if get_bool "${TEST_NESTED_KVM:=}" || (! get_bool "${TEST_NO_KVM:=}" && [[ "$(systemd-detect-virt -v)" != kvm ]]); then
+if get_bool "${TEST_NESTED_KVM:=}" || (! get_bool "${TEST_NO_KVM:=}" && ! systemd-detect-virt -qv); then
     QEMU_KVM=yes
 else
     QEMU_KVM=no
@@ -83,21 +112,20 @@ TOOLS_DIR="$SOURCE_DIR/tools"
 export TEST_BASE_DIR TEST_UNITS_DIR SOURCE_DIR TOOLS_DIR
 
 # note that find-build-dir.sh will return $BUILD_DIR if provided, else it will try to find it
-if ! BUILD_DIR="$("$TOOLS_DIR"/find-build-dir.sh)"; then
-    if get_bool "${NO_BUILD:=}"; then
-        BUILD_DIR="$SOURCE_DIR"
-    else
-        echo "ERROR: no build found, please set BUILD_DIR or use NO_BUILD" >&2
-        exit 1
-    fi
+if get_bool "${NO_BUILD:=}"; then
+    BUILD_DIR="$SOURCE_DIR"
+elif ! BUILD_DIR="$("$TOOLS_DIR"/find-build-dir.sh)"; then
+    echo "ERROR: no build found, please set BUILD_DIR or use NO_BUILD" >&2
+    exit 1
 fi
 
 PATH_TO_INIT="$ROOTLIBDIR/systemd"
 SYSTEMD_JOURNALD="${SYSTEMD_JOURNALD:-$(command -v "$BUILD_DIR/systemd-journald" || command -v "$ROOTLIBDIR/systemd-journald")}"
-SYSTEMD_JOURNAL_REMOTE="${SYSTEMD_JOURNAL_REMOTE:-$(command -v "$BUILD_DIR/systemd-journal-remote" || command -v "$ROOTLIBDIR/systemd-journal-remote")}"
+SYSTEMD_JOURNAL_REMOTE="${SYSTEMD_JOURNAL_REMOTE:-$(command -v "$BUILD_DIR/systemd-journal-remote" || command -v "$ROOTLIBDIR/systemd-journal-remote" || echo "")}"
 SYSTEMD="${SYSTEMD:-$(command -v "$BUILD_DIR/systemd" || command -v "$ROOTLIBDIR/systemd")}"
 SYSTEMD_NSPAWN="${SYSTEMD_NSPAWN:-$(command -v "$BUILD_DIR/systemd-nspawn" || command -v systemd-nspawn)}"
 JOURNALCTL="${JOURNALCTL:-$(command -v "$BUILD_DIR/journalctl" || command -v journalctl)}"
+SYSTEMCTL="${SYSTEMCTL:-$(command -v "$BUILD_DIR/systemctl" || command -v systemctl)}"
 
 TESTFILE="${BASH_SOURCE[1]}"
 if [ -z "$TESTFILE" ]; then
@@ -126,11 +154,11 @@ BASICTOOLS=(
     base64
     basename
     bash
-    busybox
     capsh
     cat
     chmod
     chown
+    chroot
     cmp
     cryptsetup
     cut
@@ -142,6 +170,7 @@ BASICTOOLS=(
     echo
     env
     false
+    flock
     getconf
     getent
     getfacl
@@ -151,9 +180,13 @@ BASICTOOLS=(
     head
     ionice
     ip
+    jq
+    killall
+    ldd
     ln
     loadkeys
     login
+    losetup
     lz4cat
     mkfifo
     mktemp
@@ -163,10 +196,12 @@ BASICTOOLS=(
     mv
     nc
     nproc
+    pkill
     readlink
     rev
     rm
     rmdir
+    rmmod
     sed
     seq
     setfattr
@@ -175,7 +210,6 @@ BASICTOOLS=(
     sfdisk
     sh
     sleep
-    socat
     stat
     su
     sulogin
@@ -192,6 +226,9 @@ BASICTOOLS=(
     umount
     uname
     unshare
+    useradd
+    userdel
+    wc
     xargs
     xzcat
 )
@@ -219,9 +256,12 @@ DEBUGTOOLS=(
     stty
     tty
     vi
+    /usr/libexec/vi
 )
 
 is_built_with_asan() {
+    local _bin="${1:?}"
+
     if ! type -P objdump >/dev/null; then
         ddebug "Failed to find objdump. Assuming systemd hasn't been built with ASAN."
         return 1
@@ -229,7 +269,7 @@ is_built_with_asan() {
 
     # Borrowed from https://github.com/google/oss-fuzz/blob/cd9acd02f9d3f6e80011cc1e9549be526ce5f270/infra/base-images/base-runner/bad_build_check#L182
     local _asan_calls
-    _asan_calls="$(objdump -dC "$SYSTEMD_JOURNALD" | grep -E "callq?\s+[0-9a-f]+\s+<__asan" -c)"
+    _asan_calls="$(objdump -dC "$_bin" | grep -E "(callq?|brasl?|bl)\s.+__asan" -c)"
     if ((_asan_calls < 1000)); then
         return 1
     else
@@ -237,13 +277,22 @@ is_built_with_asan() {
     fi
 }
 
-IS_BUILT_WITH_ASAN=$(is_built_with_asan && echo yes || echo no)
+is_built_with_coverage() {
+    if get_bool "${NO_BUILD:=}" || ! command -v meson >/dev/null; then
+        return 1
+    fi
+
+    meson configure "${BUILD_DIR:?}" | grep 'b_coverage' | awk '{ print $2 }' | grep -q 'true'
+}
+
+IS_BUILT_WITH_ASAN=$(is_built_with_asan "$SYSTEMD_JOURNALD" && echo yes || echo no)
+IS_BUILT_WITH_COVERAGE=$(is_built_with_coverage && echo yes || echo no)
 
 if get_bool "$IS_BUILT_WITH_ASAN"; then
     STRIP_BINARIES=no
     SKIP_INITRD="${SKIP_INITRD:-yes}"
     PATH_TO_INIT=$ROOTLIBDIR/systemd-under-asan
-    QEMU_MEM="2048M"
+    QEMU_MEM="${QEMU_MEM:-2G}"
     QEMU_SMP="${QEMU_SMP:-4}"
 
     # We need to correctly distinguish between gcc's and clang's ASan DSOs.
@@ -299,7 +348,7 @@ find_qemu_bin() {
     esac
 
     if [[ ! -e "$QEMU_BIN" ]]; then
-        echo "Could not find a suitable QEMU binary" >&2
+        echo "Could not find a suitable qemu binary" >&2
         return 1
     fi
 }
@@ -323,9 +372,14 @@ qemu_min_version() {
     printf "%s\n%s\n" "$1" "$qemu_ver" | sort -V -C
 }
 
-# Return 0 if QEMU did run (then you must check the result state/logs for actual
-# success), or 1 if QEMU is not available.
+# Return 0 if qemu did run (then you must check the result state/logs for actual
+# success), or 1 if qemu is not available.
 run_qemu() {
+    # If the test provided its own initrd, use it (e.g. TEST-24)
+    if [[ -z "$INITRD" && -f "${TESTDIR:?}/initrd.img" ]]; then
+        INITRD="$TESTDIR/initrd.img"
+    fi
+
     if [ -f /etc/machine-id ]; then
         read -r MACHINE_ID </etc/machine-id
         [ -z "$INITRD" ] && [ -e "$EFI_MOUNT/$MACHINE_ID/$KERNEL_VER/initrd" ] \
@@ -348,12 +402,14 @@ run_qemu() {
             [ "$ARCH" ] || ARCH=$(uname -m)
             case $ARCH in
                 ppc64*)
-                KERNEL_BIN="/boot/vmlinux-$KERNEL_VER"
-                CONSOLE=hvc0
-                ;;
+                    # Ubuntu ppc64* calls the kernel binary as vmlinux-*, RHEL/CentOS
+                    # uses the "standard" vmlinuz- prefix
+                    [[ -e "/boot/vmlinux-$KERNEL_VER" ]] && KERNEL_BIN="/boot/vmlinux-$KERNEL_VER" || KERNEL_BIN="/boot/vmlinuz-$KERNEL_VER"
+                    CONSOLE=hvc0
+                    ;;
                 *)
-                KERNEL_BIN="/boot/vmlinuz-$KERNEL_VER"
-                ;;
+                    KERNEL_BIN="/boot/vmlinuz-$KERNEL_VER"
+                    ;;
             esac
         fi
     fi
@@ -410,31 +466,36 @@ run_qemu() {
     fi
 
     kernel_params+=(
-        "root=/dev/sda1"
+        "root=LABEL=systemd_boot"
         "rw"
         "raid=noautodetect"
         "rd.luks=0"
         "loglevel=2"
         "init=$PATH_TO_INIT"
         "console=$CONSOLE"
-        "selinux=0"
         "SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/testsuite-$1.units:/usr/lib/systemd/tests/testdata/units:"
         "systemd.unit=testsuite.target"
         "systemd.wants=testsuite-$1.service"
     )
 
     if ! get_bool "$INTERACTIVE_DEBUG"; then
-        kernel_params+=("systemd.wants=end.service")
+        kernel_params+=(
+            "oops=panic"
+            "panic=1"
+            "softlockup_panic=1"
+            "systemd.wants=end.service"
+        )
     fi
 
     [ -e "$IMAGE_PRIVATE" ] && image="$IMAGE_PRIVATE" || image="$IMAGE_PUBLIC"
     qemu_options+=(
         -smp "$QEMU_SMP"
         -net none
-        -m "$QEMU_MEM"
+        -m "${QEMU_MEM:-768M}"
         -nographic
         -kernel "$KERNEL_BIN"
         -drive "format=raw,cache=unsafe,file=$image"
+        -device "virtio-rng-pci,max-bytes=1024,period=1000"
     )
 
     if [[ -n "${QEMU_OPTIONS:=}" ]]; then
@@ -445,7 +506,7 @@ run_qemu() {
 
     if [[ -n "${KERNEL_APPEND:=}" ]]; then
         local user_kernel_append
-        read -ra user_kernel_append <<< "$KERNEL_APPEND"
+        readarray user_kernel_append <<< "$KERNEL_APPEND"
         kernel_params+=("${user_kernel_append[@]}")
     fi
 
@@ -468,7 +529,7 @@ run_qemu() {
         derror "Test timed out after ${QEMU_TIMEOUT}s"
         TIMED_OUT=1
     else
-        [ "$rc" != 0 ] && derror "QEMU failed with exit code $rc"
+        [ "$rc" != 0 ] && derror "qemu failed with exit code $rc"
     fi
     return 0
 }
@@ -485,6 +546,7 @@ run_nspawn() {
         "--kill-signal=SIGKILL"
         "--directory=${1:?}"
         "--setenv=SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/testsuite-$2.units:/usr/lib/systemd/tests/testdata/units:"
+        "--machine=TEST-$TESTID"
     )
     local kernel_params=(
         "$PATH_TO_INIT"
@@ -504,7 +566,7 @@ run_nspawn() {
 
     if [[ -n "${KERNEL_APPEND:=}" ]]; then
         local user_kernel_append
-        read -ra user_kernel_append <<< "$KERNEL_APPEND"
+        readarray user_kernel_append <<< "$KERNEL_APPEND"
         kernel_params+=("${user_kernel_append[@]}")
     fi
 
@@ -565,9 +627,11 @@ install_verity_minimal() {
         oldinitdir="$initdir"
         rm -rfv "$TESTDIR/minimal"
         export initdir="$TESTDIR/minimal"
-        mkdir -p "$initdir/usr/lib/systemd/system" "$initdir/usr/lib/extension-release.d" "$initdir/etc" "$initdir/var/tmp" "$initdir/opt"
+        # app0 will use TemporaryFileSystem=/var/lib, app1 will need the mount point in the base image
+        mkdir -p "$initdir/usr/lib/systemd/system" "$initdir/usr/lib/extension-release.d" "$initdir/etc" "$initdir/var/tmp" "$initdir/opt" "$initdir/var/lib/app1"
         setup_basic_dirs
         install_basic_tools
+        install_ld_so_conf
         # Shellcheck treats [[ -v VAR ]] as an assignment to avoid a different
         # issue, thus falsely triggering SC2030 in this case
         # See: koalaman/shellcheck#1409
@@ -583,18 +647,23 @@ install_verity_minimal() {
         touch "$initdir/etc/machine-id" "$initdir/etc/resolv.conf"
         touch "$initdir/opt/some_file"
         echo MARKER=1 >>"$initdir/usr/lib/os-release"
-        echo -e "[Service]\nExecStartPre=cat /usr/lib/os-release\nExecStart=sleep 120" >"$initdir/usr/lib/systemd/system/app0.service"
-        cp "$initdir/usr/lib/systemd/system/app0.service" "$initdir/usr/lib/systemd/system/app0-foo.service"
+        echo "PORTABLE_PREFIXES=app0 minimal minimal-app0" >>"$initdir/usr/lib/os-release"
+        cat >"$initdir/usr/lib/systemd/system/minimal-app0.service" <<EOF
+[Service]
+ExecStartPre=cat /usr/lib/os-release
+ExecStart=sleep 120
+EOF
+        cp "$initdir/usr/lib/systemd/system/minimal-app0.service" "$initdir/usr/lib/systemd/system/minimal-app0-foo.service"
 
-        mksquashfs "$initdir" "$oldinitdir/usr/share/minimal_0.raw"
+        mksquashfs "$initdir" "$oldinitdir/usr/share/minimal_0.raw" -noappend
         veritysetup format "$oldinitdir/usr/share/minimal_0.raw" "$oldinitdir/usr/share/minimal_0.verity" | \
             grep '^Root hash:' | cut -f2 | tr -d '\n' >"$oldinitdir/usr/share/minimal_0.roothash"
 
         sed -i "s/MARKER=1/MARKER=2/g" "$initdir/usr/lib/os-release"
-        rm "$initdir/usr/lib/systemd/system/app0-foo.service"
-        cp "$initdir/usr/lib/systemd/system/app0.service" "$initdir/usr/lib/systemd/system/app0-bar.service"
+        rm "$initdir/usr/lib/systemd/system/minimal-app0-foo.service"
+        cp "$initdir/usr/lib/systemd/system/minimal-app0.service" "$initdir/usr/lib/systemd/system/minimal-app0-bar.service"
 
-        mksquashfs "$initdir" "$oldinitdir/usr/share/minimal_1.raw"
+        mksquashfs "$initdir" "$oldinitdir/usr/share/minimal_1.raw" -noappend
         veritysetup format "$oldinitdir/usr/share/minimal_1.raw" "$oldinitdir/usr/share/minimal_1.verity" | \
             grep '^Root hash:' | cut -f2 | tr -d '\n' >"$oldinitdir/usr/share/minimal_1.roothash"
 
@@ -613,36 +682,53 @@ install_verity_minimal() {
 Type=oneshot
 RemainAfterExit=yes
 ExecStart=/opt/script0.sh
+TemporaryFileSystem=/var/lib
+StateDirectory=app0
+RuntimeDirectory=app0
 EOF
         cat >"$initdir/opt/script0.sh" <<EOF
 #!/bin/bash
 set -e
 test -e /usr/lib/os-release
+echo bar > \${STATE_DIRECTORY}/foo
 cat /usr/lib/extension-release.d/extension-release.app0
 EOF
         chmod +x "$initdir/opt/script0.sh"
         echo MARKER=1 >"$initdir/usr/lib/systemd/system/some_file"
-        mksquashfs "$initdir" "$oldinitdir/usr/share/app0.raw"
+        mksquashfs "$initdir" "$oldinitdir/usr/share/app0.raw" -noappend
 
         export initdir="$TESTDIR/app1"
         mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
-        grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app1"
-        echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app1"
+        grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
+        ( echo "${version_id}"
+          echo "SYSEXT_SCOPE=portable"
+          echo "PORTABLE_PREFIXES=app1" ) >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
+        setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2"
         cat >"$initdir/usr/lib/systemd/system/app1.service" <<EOF
 [Service]
 Type=oneshot
 RemainAfterExit=yes
 ExecStart=/opt/script1.sh
+StateDirectory=app1
+RuntimeDirectory=app1
 EOF
         cat >"$initdir/opt/script1.sh" <<EOF
 #!/bin/bash
 set -e
 test -e /usr/lib/os-release
-cat /usr/lib/extension-release.d/extension-release.app1
+echo baz > \${STATE_DIRECTORY}/foo
+cat /usr/lib/extension-release.d/extension-release.app2
 EOF
         chmod +x "$initdir/opt/script1.sh"
         echo MARKER=1 >"$initdir/usr/lib/systemd/system/other_file"
-        mksquashfs "$initdir" "$oldinitdir/usr/share/app1.raw"
+        mksquashfs "$initdir" "$oldinitdir/usr/share/app1.raw" -noappend
+
+        export initdir="$TESTDIR/app-nodistro"
+        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system"
+        ( echo "ID=_any"
+          echo "ARCHITECTURE=_any" ) >"$initdir/usr/lib/extension-release.d/extension-release.app-nodistro"
+        echo MARKER=1 >"$initdir/usr/lib/systemd/system/some_file"
+        mksquashfs "$initdir" "$oldinitdir/usr/share/app-nodistro.raw" -noappend
     )
 }
 
@@ -660,18 +746,22 @@ setup_basic_environment() {
     install_pam
     install_dbus
     install_fonts
+    install_locales
     install_keymaps
+    install_x11_keymaps
     install_terminfo
     install_execs
     install_fs_tools
     install_modules
     install_plymouth
+    install_haveged
     install_debug_tools
     install_ld_so_conf
     install_testuser
     has_user_dbus_socket && install_user_dbus
     setup_selinux
     strip_binaries
+    instmods veth
     install_depmod_files
     generate_module_dependencies
     if get_bool "$IS_BUILT_WITH_ASAN"; then
@@ -705,9 +795,9 @@ setup_selinux() {
     mkdir -p "$initdir/usr/lib/systemd/tests/testdata/units/basic.target.wants"
     ln -sf ../autorelabel.service "$initdir/usr/lib/systemd/tests/testdata/units/basic.target.wants/"
 
-    dracut_install "${fixfiles_tools[@]}"
-    dracut_install fixfiles
-    dracut_install sestatus
+    image_install "${fixfiles_tools[@]}"
+    image_install fixfiles
+    image_install sestatus
 }
 
 install_valgrind() {
@@ -718,17 +808,17 @@ install_valgrind() {
 
     local valgrind_bins valgrind_libs valgrind_dbg_and_supp
 
-    valgrind_bins="$(strace -e execve valgrind /bin/true 2>&1 >/dev/null | perl -lne 'print $1 if /^execve\("([^"]+)"/')"
-    dracut_install "$valgrind_bins"
+    readarray -t valgrind_bins < <(strace -e execve valgrind /bin/true 2>&1 >/dev/null | perl -lne 'print $1 if /^execve\("([^"]+)"/')
+    image_install "${valgrind_bins[@]}"
 
-    valgrind_libs="$(LD_DEBUG=files valgrind /bin/true 2>&1 >/dev/null | perl -lne 'print $1 if m{calling init: (/.*vgpreload_.*)}')"
-    dracut_install "$valgrind_libs"
+    readarray -t valgrind_libs < <(LD_DEBUG=files valgrind /bin/true 2>&1 >/dev/null | perl -lne 'print $1 if m{calling init: (/.*vgpreload_.*)}')
+    image_install "${valgrind_libs[@]}"
 
-    valgrind_dbg_and_supp="$(
+    readarray -t valgrind_dbg_and_supp < <(
         strace -e open valgrind /bin/true 2>&1 >/dev/null |
         perl -lne 'if (my ($fname) = /^open\("([^"]+).*= (?!-)\d+/) { print $fname if $fname =~ /debug|\.supp$/ }'
-    )"
-    dracut_install "$valgrind_dbg_and_supp"
+    )
+    image_install "${valgrind_dbg_and_supp[@]}"
 }
 
 create_valgrind_wrapper() {
@@ -738,7 +828,7 @@ create_valgrind_wrapper() {
 #!/usr/bin/env bash
 
 mount -t proc proc /proc
-exec valgrind --leak-check=full --log-file=/valgrind.out $ROOTLIBDIR/systemd "\$@"
+exec valgrind --leak-check=full --track-fds=yes --log-file=/valgrind.out $ROOTLIBDIR/systemd "\$@"
 EOF
     chmod 0755 "$valgrind_wrapper"
 }
@@ -751,7 +841,7 @@ create_asan_wrapper() {
 
     # clang: install llvm-symbolizer to generate useful reports
     # See: https://clang.llvm.org/docs/AddressSanitizer.html#symbolizing-the-reports
-    [[ "$ASAN_COMPILER" == "clang" ]] && dracut_install "llvm-symbolizer"
+    [[ "$ASAN_COMPILER" == "clang" ]] && image_install "llvm-symbolizer"
 
     cat >"$asan_wrapper" <<EOF
 #!/usr/bin/env bash
@@ -768,6 +858,15 @@ DEFAULT_ASAN_OPTIONS=${ASAN_OPTIONS:-strict_string_checks=1:detect_stack_use_aft
 DEFAULT_UBSAN_OPTIONS=${UBSAN_OPTIONS:-print_stacktrace=1:print_summary=1:halt_on_error=1}
 DEFAULT_ENVIRONMENT="ASAN_OPTIONS=\$DEFAULT_ASAN_OPTIONS UBSAN_OPTIONS=\$DEFAULT_UBSAN_OPTIONS"
 
+# Create a simple environment file which can be included by systemd services
+# that need it (i.e. services that utilize DynamicUser=true and bash, etc.)
+cat >/usr/lib/systemd/systemd-asan-env <<INNER_EOF
+LD_PRELOAD=$ASAN_RT_PATH
+ASAN_OPTIONS=$DEFAULT_ASAN_OPTIONS
+LSAN_OPTIONS=detect_leaks=0
+UBSAN_OPTIONS=$DEFAULT_UBSAN_OPTIONS
+INNER_EOF
+
 # As right now bash is the PID 1, we can't expect PATH to have a sane value.
 # Let's make one to prevent unexpected "<bin> not found" issues in the future
 export PATH="/sbin:/bin:/usr/sbin:/usr/bin"
@@ -776,9 +875,7 @@ mount -t proc proc /proc
 mount -t sysfs sysfs /sys
 mount -o remount,rw /
 
-# A lot of services (most notably dbus) won't start without preloading libasan
-# See https://github.com/systemd/systemd/issues/5004
-DEFAULT_ENVIRONMENT="\$DEFAULT_ENVIRONMENT LD_PRELOAD=$ASAN_RT_PATH"
+DEFAULT_ENVIRONMENT="\$DEFAULT_ENVIRONMENT ASAN_RT_PATH=$ASAN_RT_PATH"
 
 if [[ "$ASAN_COMPILER" == "clang" ]]; then
   # Let's add the ASan DSO's path to the dynamic linker's cache. This is pretty
@@ -815,38 +912,6 @@ printf "[Unit]\nConditionVirtualization=container\n\n[Service]\nTimeoutSec=240s\
 mkdir -p /etc/systemd/system/systemd-journal-flush.service.d
 printf "[Service]\nTimeoutSec=180s\n" >/etc/systemd/system/systemd-journal-flush.service.d/timeout.conf
 
-# D-Bus has troubles during system shutdown causing it to fail. Although it's
-# harmless, it causes unnecessary noise in the logs, so let's disable LSan's
-# at_exit check just for the dbus.service
-mkdir -p /etc/systemd/system/dbus.service.d
-printf "[Service]\nEnvironment=ASAN_OPTIONS=leak_check_at_exit=false\n" >/etc/systemd/system/dbus.service.d/disable-lsan.conf
-
-# Some utilities run via IMPORT/RUN/PROGRAM udev directives fail because
-# they're uninstrumented (like dmsetup). Let's add a simple rule which sets
-# LD_PRELOAD to the ASan RT library to fix this.
-mkdir -p /etc/udev/rules.d
-cat >/etc/udev/rules.d/00-set-LD_PRELOAD.rules <<INNER_EOF
-SUBSYSTEM=="block", ENV{LD_PRELOAD}="$ASAN_RT_PATH"
-INNER_EOF
-chmod 0644 /etc/udev/rules.d/00-set-LD_PRELOAD.rules
-
-# The 'mount' utility doesn't behave well under libasan, causing unexpected
-# fails during boot and subsequent test results check:
-# bash-5.0# mount -o remount,rw -v /
-# mount: /dev/sda1 mounted on /.
-# bash-5.0# echo \$?
-# 1
-# Let's workaround this by clearing the previously set LD_PRELOAD env variable,
-# so the libasan library is not loaded for this particular service
-unset_ld_preload() {
-    local _dropin_dir="/etc/systemd/system/\$1.service.d"
-    mkdir -p "\$_dropin_dir"
-    printf "[Service]\nUnsetEnvironment=LD_PRELOAD\n" >"\$_dropin_dir/unset_ld_preload.conf"
-}
-
-unset_ld_preload systemd-remount-fs
-unset_ld_preload testsuite-
-
 export ASAN_OPTIONS=\$DEFAULT_ASAN_OPTIONS:log_path=/systemd.asan.log UBSAN_OPTIONS=\$DEFAULT_UBSAN_OPTIONS
 exec "$ROOTLIBDIR/systemd" "\$@"
 EOF
@@ -867,15 +932,15 @@ EOF
 
 install_fs_tools() {
     dinfo "Install fsck"
-    dracut_install /sbin/fsck*
-    dracut_install -o /bin/fsck*
+    image_install /sbin/fsck*
+    image_install -o /bin/fsck*
 
     # fskc.reiserfs calls reiserfsck. so, install it
-    dracut_install -o reiserfsck
+    image_install -o reiserfsck
 
     # we use mkfs in system-repart tests
-    dracut_install /sbin/mkfs.ext4
-    dracut_install /sbin/mkfs.vfat
+    image_install /sbin/mkfs.ext4
+    image_install /sbin/mkfs.vfat
 }
 
 install_modules() {
@@ -885,6 +950,8 @@ install_modules() {
     instmods vfat
     instmods nls_ascii =nls
     instmods dummy
+    # for TEST-35-LOGIN
+    instmods scsi_debug uinput
 
     if get_bool "$LOOKS_LIKE_SUSE"; then
         instmods ext4
@@ -894,6 +961,7 @@ install_modules() {
 install_dmevent() {
     instmods dm_crypt =crypto
     inst_binary dmeventd
+    image_install "${ROOTLIBDIR:?}"/system/dm-event.{service,socket}
     if get_bool "$LOOKS_LIKE_DEBIAN"; then
         # dmsetup installs 55-dm and 60-persistent-storage-dm on Debian/Ubuntu
         # and since buster/bionic 95-dm-notify.rules
@@ -907,6 +975,140 @@ install_dmevent() {
     fi
 }
 
+install_multipath() {
+    instmods "=md" multipath
+    image_install kpartx /lib/udev/kpartx_id lsmod mpathpersist multipath multipathd partx
+    image_install "${ROOTLIBDIR:?}"/system/multipathd.{service,socket}
+    if get_bool "$LOOKS_LIKE_DEBIAN"; then
+        inst_rules 56-dm-parts.rules 56-dm-mpath.rules 60-multipath.rules 68-del-part-nodes.rules 95-kpartx.rules
+    else
+        inst_rules 11-dm-mpath.rules 11-dm-parts.rules 62-multipath.rules 66-kpartx.rules 68-del-part-nodes.rules
+    fi
+    mkdir -p "${initdir:?}/etc/multipath"
+
+    local file
+    while read -r file; do
+        # Install libraries required by the given library
+        inst_libs "$file"
+        # Install the library itself and create necessary symlinks
+        inst_library "$file"
+    done < <(find /lib*/multipath -type f)
+}
+
+install_lvm() {
+    image_install lvm
+    image_install "${ROOTLIBDIR:?}"/system/lvm2-lvmpolld.{service,socket}
+    image_install "${ROOTLIBDIR:?}"/system/{blk-availability,lvm2-monitor}.service
+    image_install -o "/lib/tmpfiles.d/lvm2.conf"
+    if get_bool "$LOOKS_LIKE_DEBIAN"; then
+        inst_rules 56-lvm.rules 69-lvm-metad.rules
+    else
+        # Support the new udev autoactivation introduced in lvm 2.03.14
+        # https://sourceware.org/git/?p=lvm2.git;a=commit;h=67722b312390cdab29c076c912e14bd739c5c0f6
+        # Static autoactivation (via lvm2-activation-generator) was dropped
+        # in lvm 2.03.15
+        # https://sourceware.org/git/?p=lvm2.git;a=commit;h=ee8fb0310c53ed003a43b324c99cdfd891dd1a7c
+        if [[ -f /lib/udev/rules.d/69-dm-lvm.rules ]]; then
+            inst_rules 11-dm-lvm.rules 69-dm-lvm.rules
+        else
+            image_install "${ROOTLIBDIR:?}"/system-generators/lvm2-activation-generator
+            image_install "${ROOTLIBDIR:?}"/system/lvm2-pvscan@.service
+            inst_rules 11-dm-lvm.rules 69-dm-lvm-metad.rules
+        fi
+    fi
+    mkdir -p "${initdir:?}/etc/lvm"
+}
+
+install_btrfs() {
+    instmods btrfs
+    # Not all utilities provided by btrfs-progs are listed here; extend the list
+    # if necessary
+    image_install btrfs btrfstune mkfs.btrfs
+    inst_rules 64-btrfs-dm.rules
+}
+
+install_iscsi() {
+    # Install both client and server side stuff by default
+    local inst="${1:-}"
+    local file
+
+    # Install client-side stuff ("initiator" in iSCSI jargon) - Open-iSCSI in this case
+    # (open-iscsi on Debian, iscsi-initiator-utils on Fedora, etc.)
+    if [[ -z "$inst" || "$inst" =~ (client|initiator) ]]; then
+        image_install iscsi-iname iscsiadm iscsid iscsistart
+        image_install -o "${ROOTLIBDIR:?}"/system/iscsi-{init,onboot,shutdown}.service
+        image_install "${ROOTLIBDIR:?}"/system/iscsid.{service,socket}
+        image_install "${ROOTLIBDIR:?}"/system/iscsi.service
+        mkdir -p "${initdir:?}"/var/lib/iscsi/{ifaces,isns,nodes,send_targets,slp,static}
+        mkdir -p "${initdir:?}/etc/iscsi"
+        echo "iscsid.startup = /bin/systemctl start iscsid.socket" >"${initdir:?}/etc/iscsi/iscsid.conf"
+        # Since open-iscsi 2.1.2 [0] the initiator name should be generated via
+        # a one-time service instead of distro package's post-install scripts.
+        # However, some distros still use this approach even after this patch,
+        # so prefer the already existing initiatorname.iscsi file if it exists.
+        #
+        # [0] https://github.com/open-iscsi/open-iscsi/commit/f37d5b653f9f251845db3f29b1a3dcb90ec89731
+        if [[ ! -e /etc/iscsi/initiatorname.iscsi ]]; then
+            image_install "${ROOTLIBDIR:?}"/system/iscsi-init.service
+            if get_bool "$IS_BUILT_WITH_ASAN"; then
+                # The iscsi-init.service calls `sh` which might, in certain circumstances,
+                # pull in instrumented systemd NSS modules causing `sh` to fail. Let's mitigate
+                # this by pulling in an env file crafted by `create_asan_wrapper()` that
+                # (among others) pre-loads ASan's DSO.
+                mkdir -p "${initdir:?}/etc/systemd/system/iscsi-init.service.d/"
+                printf "[Service]\nEnvironmentFile=/usr/lib/systemd/systemd-asan-env" >"${initdir:?}/etc/systemd/system/iscsi-init.service.d/asan-env.conf"
+            fi
+        else
+            inst_simple "/etc/iscsi/initiatorname.iscsi"
+        fi
+    fi
+
+    # Install server-side stuff ("target" in iSCSI jargon) - TGT in this case
+    # (tgt on Debian, scsi-target-utils on Fedora, etc.)
+    if [[ -z "$inst" || "$inst" =~ (server|target) ]]; then
+        image_install tgt-admin tgt-setup-lun tgtadm tgtd tgtimg
+        image_install -o /etc/sysconfig/tgtd
+        image_install "${ROOTLIBDIR:?}"/system/tgtd.service
+        mkdir -p "${initdir:?}/etc/tgt"
+        touch "${initdir:?}"/etc/tgt/{tgtd,targets}.conf
+        # Install perl modules required by tgt-admin
+        #
+        # Forgive me father for I have sinned. The monstrosity below appends
+        # a perl snippet to the `tgt-admin` perl script on the fly, which
+        # dumps a list of files (perl modules) required by `tgt-admin` at
+        # the runtime plus any DSOs loaded via DynaLoader. This list is then
+        # passed to `inst_simple` which installs the necessary files into the image
+        #
+        # shellcheck disable=SC2016
+        while read -r file; do
+            inst_simple "$file"
+        done < <(perl -- <(cat "$(command -v tgt-admin)" <(echo -e 'use DynaLoader; print map { "$_\n" } values %INC; print join("\n", @DynaLoader::dl_shared_objects)')) -p | awk '/^\// { print $1 }')
+    fi
+}
+
+install_mdadm() {
+    local unit
+    local mdadm_units=(
+        system/mdadm-grow-continue@.service
+        system/mdadm-last-resort@.service
+        system/mdadm-last-resort@.timer
+        system/mdmon@.service
+        system/mdmonitor-oneshot.service
+        system/mdmonitor-oneshot.timer
+        system/mdmonitor.service
+        system-shutdown/mdadm.shutdown
+    )
+
+    image_install mdadm mdmon
+    inst_rules 01-md-raid-creating.rules 63-md-raid-arrays.rules 64-md-raid-assembly.rules 69-md-clustered-confirm-device.rules
+    # Fedora/CentOS/RHEL ships this rule file
+    [[ -f /lib/udev/rules.d/65-md-incremental.rules ]] && inst_rules 65-md-incremental.rules
+
+    for unit in "${mdadm_units[@]}"; do
+        image_install "${ROOTLIBDIR:?}/$unit"
+    done
+}
+
 install_compiled_systemd() {
     dinfo "Install compiled systemd"
 
@@ -917,6 +1119,16 @@ install_compiled_systemd() {
         exit 1
     fi
     (set -x; DESTDIR="$initdir" "$ninja_bin" -C "$BUILD_DIR" install)
+
+    # If we are doing coverage runs, copy over the binary notes files, as lcov expects to
+    # find them in the same directory as the runtime data counts
+    if get_bool "$IS_BUILT_WITH_COVERAGE"; then
+        mkdir -p "${initdir}/${BUILD_DIR:?}/"
+        rsync -am --include='*/' --include='*.gcno' --exclude='*' "${BUILD_DIR:?}/" "${initdir}/${BUILD_DIR:?}/"
+        # Set effective & default ACLs for the build dir so unprivileged
+        # processes can write gcda files with coverage stats
+        setfacl -R -m 'd:o:rwX' -m 'o:rwX' "${initdir}/${BUILD_DIR:?}/"
+    fi
 }
 
 install_debian_systemd() {
@@ -935,11 +1147,52 @@ install_debian_systemd() {
     done < <(grep -E '^Package:' "${SOURCE_DIR}/debian/control" | cut -d ':' -f 2)
 }
 
+install_suse_systemd() {
+    local testsdir=/usr/lib/systemd/tests
+    local pkgs
+
+    dinfo "Install SUSE systemd"
+
+    pkgs=(
+        systemd
+        systemd-container
+        systemd-coredump
+        systemd-experimental
+        systemd-journal-remote
+        systemd-portable
+        udev
+    )
+
+    for p in "${pkgs[@]}"; do
+        rpm -q "$p" &>/dev/null || continue
+
+        ddebug "Install files from package $p"
+        while read -r f; do
+            [ -e "$f" ] || continue
+            [ -d "$f" ] && continue
+            inst "$f"
+        done < <(rpm -ql "$p")
+    done
+
+    # we only need testsdata dir as well as the unit tests (for
+    # TEST-02-UNITTESTS) in the image.
+    dinfo "Install unit tests and testdata directory"
+
+    mkdir -p "$initdir/$testsdir"
+    cp "$testsdir"/test-* "$initdir/$testsdir/"
+    cp -a "$testsdir/testdata" "$initdir/$testsdir/"
+
+    # On openSUSE, these dirs are not created at package install for now on.
+    mkdir -p "$initdir/var/log/journal/remote"
+}
+
 install_distro_systemd() {
     dinfo "Install distro systemd"
 
     if get_bool "$LOOKS_LIKE_DEBIAN"; then
         install_debian_systemd
+    elif get_bool "$LOOKS_LIKE_SUSE"; then
+        install_suse_systemd
     else
         dfatal "NO_BUILD not supported for this distro"
         exit 1
@@ -955,14 +1208,43 @@ install_systemd() {
     fi
 
     # remove unneeded documentation
-    rm -fr "$initdir"/usr/share/{man,doc}
-
-    get_bool "$LOOKS_LIKE_SUSE" && setup_suse
+    rm -fr "${initdir:?}"/usr/share/{man,doc}
 
     # enable debug logging in PID1
     echo LogLevel=debug >>"$initdir/etc/systemd/system.conf"
+    if [[ -n "$TEST_SYSTEMD_LOG_LEVEL" ]]; then
+        echo DefaultEnvironment=SYSTEMD_LOG_LEVEL="$TEST_SYSTEMD_LOG_LEVEL" >>"$initdir/etc/systemd/system.conf"
+    fi
     # store coredumps in journal
     echo Storage=journal >>"$initdir/etc/systemd/coredump.conf"
+    # Propagate SYSTEMD_UNIT_PATH to user systemd managers
+    mkdir "$initdir/etc/systemd/system/user@.service.d/"
+    echo -e "[Service]\nPassEnvironment=SYSTEMD_UNIT_PATH\n" >"$initdir/etc/systemd/system/user@.service.d/override.conf"
+
+    # When built with gcov, disable ProtectSystem= and ProtectHome= in the test
+    # images, since it prevents gcov to write the coverage reports (*.gcda
+    # files)
+    if get_bool "$IS_BUILT_WITH_COVERAGE"; then
+        mkdir -p "$initdir/etc/systemd/system/service.d/"
+        echo -e "[Service]\nProtectSystem=no\nProtectHome=no\n" >"$initdir/etc/systemd/system/service.d/99-gcov-override.conf"
+        # Similarly, set ReadWritePaths= to the $BUILD_DIR in the test image
+        # to make the coverage work with units utilizing DynamicUser=yes. Do
+        # this only for services from TEST-20, as setting this system-wide
+        # has many undesirable side-effects
+        mkdir -p "$initdir/etc/systemd/system/test20-.service.d/"
+        echo -e "[Service]\nReadWritePaths=${BUILD_DIR:?}\n" >"$initdir/etc/systemd/system/test20-.service.d/99-gcov-rwpaths-override.conf"
+    fi
+
+    # If we're built with -Dportabled=false, tests with systemd-analyze
+    # --profile will fail. Since we need just the profile (text) files, let's
+    # copy them into the image if they don't exist there.
+    local portable_dir="${initdir:?}${ROOTLIBDIR:?}/portable"
+    if [[ ! -d "$portable_dir/profile/strict" ]]; then
+        dinfo "Couldn't find portable profiles in the test image"
+        dinfo "Copying them directly from the source tree"
+        mkdir -p "$portable_dir"
+        cp -frv "${SOURCE_DIR:?}/src/portable/profile" "$portable_dir"
+    fi
 }
 
 get_ldpath() {
@@ -983,10 +1265,18 @@ install_missing_libraries() {
         LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$(get_ldpath "$i")" inst_libs "$i"
     done
 
+    # Install libgcc_s.so if available, since it's dlopen()ed by libpthread
+    # and might cause unexpected failures during pthread_exit()/pthread_cancel()
+    # if not present
+    # See: https://github.com/systemd/systemd/pull/23858
+    while read -r libgcc_s; do
+        [[ -e "$libgcc_s" ]] && inst_library "$libgcc_s"
+    done < <(ldconfig -p | awk '/\/libgcc_s.so.1$/ { print $4 }')
+
     local lib path
     # A number of dependencies is now optional via dlopen, so the install
     # script will not pick them up, since it looks at linkage.
-    for lib in libcryptsetup libidn libidn2 pwquality libqrencode tss2-esys tss2-rc tss2-mu libfido2 libbpf; do
+    for lib in libcryptsetup libidn libidn2 pwquality libqrencode tss2-esys tss2-rc tss2-mu tss2-tcti-device libfido2 libbpf libelf libdw xkbcommon; do
         ddebug "Searching for $lib via pkg-config"
         if pkg-config --exists "$lib"; then
                 path="$(pkg-config --variable=libdir "$lib")"
@@ -1002,11 +1292,24 @@ install_missing_libraries() {
                 # (eg: libcryptsetup), so just ignore them
                 inst_libs "${path}/${lib}.so" || true
                 inst_library "${path}/${lib}.so" || true
+
+                if [[ "$lib" == "libxkbcommon" ]]; then
+                    install_x11_keymaps full
+                fi
         else
             ddebug "$lib.pc not found, skipping"
             continue
         fi
     done
+
+    # Install extra openssl 3 stuff
+    path="$(pkg-config --variable=libdir libcrypto)"
+    inst_simple "${path}/ossl-modules/legacy.so" || true
+    inst_simple "${path}/ossl-modules/fips.so" || true
+    inst_simple "${path}/engines-3/afalg.so" || true
+    inst_simple "${path}/engines-3/capi.so" || true
+    inst_simple "${path}/engines-3/loader_attic.so" || true
+    inst_simple "${path}/engines-3/padlock.so" || true
 }
 
 cleanup_loopdev() {
@@ -1017,7 +1320,7 @@ cleanup_loopdev() {
     fi
 }
 
-trap cleanup_loopdev EXIT INT QUIT PIPE
+add_at_exit_handler cleanup_loopdev
 
 create_empty_image() {
     if [ -z "${IMAGE_NAME:=}" ]; then
@@ -1025,37 +1328,45 @@ create_empty_image() {
         exit 1
     fi
 
-    local size=500
+    # Partition sizes are in MiBs
+    local root_size=1000
+    local data_size=50
     if ! get_bool "$NO_BUILD"; then
         if meson configure "${BUILD_DIR:?}" | grep 'static-lib\|standalone-binaries' | awk '{ print $2 }' | grep -q 'true'; then
-            size=$((size+=200))
+            root_size=$((root_size+=200))
         fi
         if meson configure "${BUILD_DIR:?}" | grep 'link-.*-shared' | awk '{ print $2 }' | grep -q 'false'; then
-            size=$((size+=200))
+            root_size=$((root_size+=200))
+        fi
+        if get_bool "$IS_BUILT_WITH_COVERAGE"; then
+            root_size=$((root_size+=250))
         fi
     fi
     if ! get_bool "$STRIP_BINARIES"; then
-        size=$((4 * size))
+        root_size=$((4 * root_size))
+        data_size=$((2 * data_size))
     fi
 
-    echo "Setting up ${IMAGE_PUBLIC:?} (${size} MB)"
+    echo "Setting up ${IMAGE_PUBLIC:?} (${root_size} MB)"
     rm -f "${IMAGE_PRIVATE:?}" "$IMAGE_PUBLIC"
 
     # Create the blank file to use as a root filesystem
-    truncate -s "${size}M" "$IMAGE_PUBLIC"
+    truncate -s "${root_size}M" "$IMAGE_PUBLIC"
 
     LOOPDEV=$(losetup --show -P -f "$IMAGE_PUBLIC")
     [ -b "$LOOPDEV" ] || return 1
+    # Create two partitions - a root one and a data one (utilized by some tests)
     sfdisk "$LOOPDEV" <<EOF
-,$((size - 50))M
-,
+label: gpt
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=root size=$((root_size - data_size))M bootable
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=data
 EOF
 
     udevadm settle
 
-    local label=(-L systemd)
+    local label=(-L systemd_boot)
     # mkfs.reiserfs doesn't know -L. so, use --label instead
-    [[ "$FSTYPE" == "reiserfs" ]] && label=(--label systemd)
+    [[ "$FSTYPE" == "reiserfs" ]] && label=(--label systemd_boot)
     if ! mkfs -t "${FSTYPE}" "${label[@]}" "${LOOPDEV}p1" -q; then
         dfatal "Failed to mkfs -t ${FSTYPE}"
         exit 1
@@ -1080,7 +1391,9 @@ mount_initdir() {
 
 cleanup_initdir() {
     # only umount if create_empty_image_rootdir() was called to mount it
-    get_bool "$TEST_SETUP_CLEANUP_ROOTDIR" && _umount_dir "${initdir:?}"
+    if get_bool "$TEST_SETUP_CLEANUP_ROOTDIR"; then
+        _umount_dir "${initdir:?}"
+    fi
 }
 
 umount_loopback() {
@@ -1120,6 +1433,7 @@ check_asan_reports() {
                  BEGIN {
                      %services_to_ignore = (
                          "dbus-daemon" => undef,
+                         "dbus-broker-launch" => undef,
                      );
                  }
                  print $2 if /\s(\S*)\[(\d+)\]:\s*SUMMARY:\s+\w+Sanitizer/ && !exists $services_to_ignore{$1}'
@@ -1135,6 +1449,55 @@ check_asan_reports() {
     return $ret
 }
 
+check_coverage_reports() {
+    local root="${1:?}"
+
+    if get_bool "$NO_BUILD"; then
+        return 0
+    fi
+    if ! get_bool "$IS_BUILT_WITH_COVERAGE"; then
+        return 0
+    fi
+
+    if [ -n "${ARTIFACT_DIRECTORY}" ]; then
+        dest="${ARTIFACT_DIRECTORY}/${testname:?}.coverage-info"
+    else
+        dest="${TESTDIR:?}/coverage-info"
+    fi
+
+    # Create a coverage report that will later be uploaded. Remove info about
+    # system libraries/headers, as we don't really care about them.
+    if [[ -f "$dest" ]]; then
+        # If the destination report file already exists, don't overwrite it, but
+        # dump the new report in a temporary file and then merge it with the already
+        # present one - this usually happens when running both "parts" of a test
+        # in one run (the qemu and the nspawn part).
+        lcov --directory "${root}/${BUILD_DIR:?}" --capture --output-file "${dest}.new"
+        lcov --remove "${dest}.new" -o "${dest}.new" '/usr/include/*' '/usr/lib/*'
+        lcov --add-tracefile "${dest}" --add-tracefile "${dest}.new" -o "${dest}"
+        rm -f "${dest}.new"
+    else
+        lcov --directory "${root}/${BUILD_DIR:?}" --capture --output-file "${dest}"
+        lcov --remove "${dest}" -o "${dest}" '/usr/include/*' '/usr/lib/*'
+    fi
+
+    # If the test logs contain lines like:
+    #
+    # ...systemd-resolved[735885]: profiling:/systemd-meson-build/src/shared/libsystemd-shared-250.a.p/base-filesystem.c.gcda:Cannot open
+    #
+    # it means we're possibly missing some coverage since gcov can't write the stats,
+    # usually due to the sandbox being too restrictive (e.g. ProtectSystem=yes,
+    # ProtectHome=yes) or the $BUILD_DIR being inaccessible to non-root users - see
+    # `setfacl` stuff in install_compiled_systemd().
+    if ! get_bool "${IGNORE_MISSING_COVERAGE:=}" && \
+       "${JOURNALCTL:?}" -q --no-pager -D "${root:?}/var/log/journal" --grep "profiling:.+?gcda:[Cc]annot open"; then
+        derror "Detected possibly missing coverage, check the journal"
+        return 1
+    fi
+
+    return 0
+}
+
 save_journal() {
     # Default to always saving journal
     local save="yes"
@@ -1153,7 +1516,11 @@ save_journal() {
 
     for j in "${1:?}"/*; do
         if get_bool "$save"; then
-            "$SYSTEMD_JOURNAL_REMOTE" -o "$dest" --getter="$JOURNALCTL -o export -D $j"
+            if [ "$SYSTEMD_JOURNAL_REMOTE" = "" ]; then
+                cp -a "$j" "$dest"
+            else
+                "$SYSTEMD_JOURNAL_REMOTE" -o "$dest" --getter="$JOURNALCTL -o export -D $j"
+            fi
         fi
 
         if [ -n "${TEST_SHOW_JOURNAL}" ]; then
@@ -1187,6 +1554,9 @@ check_result_common() {
             setfacl -m "user:${SUDO_USER:?}:r-X" "${TESTDIR:?}/"failed
         fi
         ret=1
+    elif get_bool "$TIMED_OUT"; then
+        echo "(timeout)" >"${TESTDIR:?}/failed"
+        ret=2
     elif [ -e "$workspace/testok" ]; then
         # â€¦/testok always counts (but with lower priority than â€¦/failed)
         ret=0
@@ -1195,9 +1565,6 @@ check_result_common() {
         echo "${TESTNAME:?} was skipped:"
         cat "$workspace/skipped"
         ret=0
-    elif get_bool "$TIMED_OUT"; then
-        echo "(timeout)" >"${TESTDIR:?}/failed"
-        ret=2
     else
         echo "(failed; see logs)" >"${TESTDIR:?}/failed"
         ret=3
@@ -1205,6 +1572,8 @@ check_result_common() {
 
     check_asan_reports "$workspace" || ret=4
 
+    check_coverage_reports "$workspace" || ret=5
+
     save_journal "$workspace/var/log/journal" $ret
 
     if [ -d "${ARTIFACT_DIRECTORY}" ] && [ -f "$workspace/strace.out" ]; then
@@ -1224,17 +1593,17 @@ check_result_nspawn() {
     local workspace="${1:?}"
     local ret
 
-    check_result_common "${workspace}"
-    ret=$?
-
-    # Run additional test-specific checks if defined by check_result_nspawn_hook()
+    # Run a test-specific checks if defined by check_result_nspawn_hook()
     if declare -F check_result_nspawn_hook >/dev/null; then
-        if ! check_result_nspawn_hook; then
+        if ! check_result_nspawn_hook "${workspace}"; then
             derror "check_result_nspawn_hook() returned with EC > 0"
             ret=4
         fi
     fi
 
+    check_result_common "${workspace}"
+    ret=$?
+
     _umount_dir "${initdir:?}"
 
     return $ret
@@ -1245,19 +1614,19 @@ check_result_qemu() {
     local ret
     mount_initdir
 
-    check_result_common "${initdir:?}"
-    ret=$?
-
-    _umount_dir "${initdir:?}"
-
-    # Run additional test-specific checks if defined by check_result_qemu_hook()
+    # Run a test-specific checks if defined by check_result_qemu_hook()
     if declare -F check_result_qemu_hook >/dev/null; then
-        if ! check_result_qemu_hook; then
+        if ! check_result_qemu_hook "${initdir:?}"; then
             derror "check_result_qemu_hook() returned with EC > 0"
             ret=4
         fi
     fi
 
+    check_result_common "${initdir:?}"
+    ret=$?
+
+    _umount_dir "${initdir:?}"
+
     return $ret
 }
 
@@ -1285,6 +1654,7 @@ check_result_nspawn_unittests() {
     fi
 
     get_bool "${TIMED_OUT:=}" && ret=1
+    check_coverage_reports "$workspace" || ret=5
 
     save_journal "$workspace/var/log/journal" $ret
 
@@ -1317,6 +1687,7 @@ check_result_qemu_unittests() {
     fi
 
     get_bool "${TIMED_OUT:=}" && ret=1
+    check_coverage_reports "$initdir" || ret=5
 
     save_journal "$initdir/var/log/journal" $ret
 
@@ -1383,12 +1754,24 @@ install_plymouth() {
     # if [ -x /usr/libexec/plymouth/plymouth-populate-initrd ]; then
     #     PLYMOUTH_POPULATE_SOURCE_FUNCTIONS="$TEST_BASE_DIR/test-functions" \
     #         /usr/libexec/plymouth/plymouth-populate-initrd -t $initdir
-    #         dracut_install plymouth plymouthd
+    #         image_install plymouth plymouthd
     # else
         rm -f "${initdir:?}"/{usr/lib,lib,etc}/systemd/system/plymouth* "$initdir"/{usr/lib,lib,etc}/systemd/system/*/plymouth*
     # fi
 }
 
+install_haveged() {
+    # If haveged is installed, it's probably included in initrd and needs to be
+    # installed in the image too.
+    if [ -x /usr/sbin/haveged ]; then
+        dinfo "Install haveged files"
+        inst /usr/sbin/haveged
+        for u in /usr/lib/systemd/system/haveged*; do
+            inst "$u"
+        done
+    fi
+}
+
 install_ld_so_conf() {
     dinfo "Install /etc/ld.so.conf*"
     cp -a /etc/ld.so.conf* "${initdir:?}/etc"
@@ -1437,29 +1820,47 @@ install_config_files() {
 
 install_basic_tools() {
     dinfo "Install basic tools"
-    dracut_install "${BASICTOOLS[@]}"
-    dracut_install -o sushell
+    image_install "${BASICTOOLS[@]}"
+    image_install -o sushell
     # in Debian ldconfig is just a shell script wrapper around ldconfig.real
-    dracut_install -o ldconfig.real
+    image_install -o ldconfig.real
 }
 
 install_debug_tools() {
     dinfo "Install debug tools"
-    dracut_install "${DEBUGTOOLS[@]}"
+    image_install -o "${DEBUGTOOLS[@]}"
 
     if get_bool "$INTERACTIVE_DEBUG"; then
         # Set default TERM from vt220 to linux, so at least basic key shortcuts work
         local getty_override="${initdir:?}/etc/systemd/system/serial-getty@.service.d"
         mkdir -p "$getty_override"
         echo -e "[Service]\nEnvironment=TERM=linux" >"$getty_override/default-TERM.conf"
+        echo 'export TERM=linux' >>"$initdir/etc/profile"
 
-        cat >"$initdir/etc/motd" <<EOF
-To adjust the terminal size use:
-    export COLUMNS=xx
-    export LINES=yy
-or
-    stty cols xx rows yy
+        if command -v resize >/dev/null; then
+            image_install resize
+            echo "resize" >>"$initdir/etc/profile"
+        fi
+
+        # Sometimes we might end up with plymouthd still running (especially
+        # with the initrd -> asan_wrapper -> systemd transition), which will eat
+        # our inputs and make debugging via tty impossible. Let's fix this by
+        # killing plymouthd explicitly for the interactive sessions.
+        # Note: we can't use pkill/pidof/etc. here due to a bug in libasan, see:
+        #   - https://github.com/llvm/llvm-project/issues/49223
+        #   - https://bugzilla.redhat.com/show_bug.cgi?id=2098125
+        local plymouth_unit="${initdir:?}/etc/systemd/system/kill-plymouth.service"
+        cat >"$plymouth_unit" <<EOF
+[Unit]
+After=multi-user.target
+
+[Service]
+ExecStart=sh -c 'killall --verbose plymouthd || :'
+
+[Install]
+WantedBy=multi-user.target
 EOF
+        "${SYSTEMCTL:?}" enable --root "${initdir:?}" kill-plymouth.service
     fi
 }
 
@@ -1468,7 +1869,9 @@ install_libnss() {
     # install libnss_files for login
     local NSS_LIBS
     mapfile -t NSS_LIBS < <(LD_DEBUG=files getent passwd 2>&1 >/dev/null | sed -n '/calling init: .*libnss_/ {s!^.* /!/!; p}')
-    dracut_install "${NSS_LIBS[@]}"
+    if [[ ${#NSS_LIBS[@]} -gt 0 ]]; then
+        image_install "${NSS_LIBS[@]}"
+    fi
 }
 
 install_dbus() {
@@ -1499,13 +1902,28 @@ install_dbus() {
     cat >"$initdir/etc/dbus-1/system.d/systemd.test.ExecStopPost.conf" <<EOF
 <?xml version="1.0"?>
 <!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
-        "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+        "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
 <busconfig>
     <policy user="root">
         <allow own="systemd.test.ExecStopPost"/>
     </policy>
 </busconfig>
 EOF
+
+    # If we run without KVM, bump the service start timeout
+    if ! get_bool "$QEMU_KVM"; then
+        cat >"$initdir/etc/dbus-1/system.d/service.timeout.conf" <<EOF
+<?xml version="1.0"?>
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+        "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+    <limit name="service_start_timeout">60000</limit>
+</busconfig>
+EOF
+        # Bump the client-side timeout in sd-bus as well
+        mkdir -p "$initdir/etc/systemd/system.conf.d"
+        echo -e '[Manager]\nDefaultEnvironment=SYSTEMD_BUS_TIMEOUT=60' >"$initdir/etc/systemd/system.conf.d/bus-timeout.conf"
+    fi
 }
 
 install_user_dbus() {
@@ -1550,7 +1968,7 @@ install_pam() {
         paths+=(/lib*/security)
     fi
 
-    for d in /etc/pam.d /etc/security /usr/lib/pam.d; do
+    for d in /etc/pam.d /{usr/,}etc/security /usr/{etc,lib}/pam.d; do
         [ -d "$d" ] && paths+=("$d")
     done
 
@@ -1560,37 +1978,107 @@ install_pam() {
 
     # pam_unix depends on unix_chkpwd.
     # see http://www.linux-pam.org/Linux-PAM-html/sag-pam_unix.html
-    dracut_install -o unix_chkpwd
+    image_install -o unix_chkpwd
 
     # set empty root password for easy debugging
     sed -i 's/^root:x:/root::/' "${initdir:?}/etc/passwd"
+
+    # And make sure pam_unix will accept it by making sure that
+    # the PAM module has the nullok option.
+    for d in /etc/pam.d /usr/{etc,lib}/pam.d; do
+        [ -d "$initdir/$d" ] || continue
+        sed -i '/^auth.*pam_unix.so/s/$/ nullok/' "$initdir/$d"/*
+    done
 }
 
+install_locales() {
+    # install only C.UTF-8 and English locales
+    dinfo "Install locales"
+
+    if command -v meson >/dev/null \
+            && (meson configure "${BUILD_DIR:?}" | grep 'localegen-path */') \
+            || get_bool "$LOOKS_LIKE_DEBIAN"; then
+        # locale-gen support
+        image_install -o locale-gen localedef
+        inst /etc/locale.gen || :
+        inst /usr/share/i18n/SUPPORTED || :
+        inst_recursive /usr/share/i18n/charmaps
+        inst_recursive /usr/share/i18n/locales
+        inst_recursive /usr/share/locale/en*
+        inst_recursive /usr/share/locale/de*
+        image_install /usr/share/locale/locale.alias
+        # locale-gen might either generate each locale separately or merge them
+        # into a single archive
+        if ! (inst_recursive /usr/lib/locale/C.*8 /usr/lib/locale/en_*8 ||
+              image_install /usr/lib/locale/locale-archive); then
+            dfatal "Failed to install required locales"
+            exit 1
+        fi
+    else
+        inst_recursive /usr/lib/locale/C.*8 /usr/lib/locale/en_*8
+    fi
+}
+
+# shellcheck disable=SC2120
 install_keymaps() {
-    dinfo "Install keymaps"
-    # The first three paths may be deprecated.
-    # It seems now the last two paths are used by many distributions.
-    for i in \
-        /usr/lib/kbd/keymaps/include/* \
-        /usr/lib/kbd/keymaps/i386/include/* \
-        /usr/lib/kbd/keymaps/i386/qwerty/us.* \
-        /usr/lib/kbd/keymaps/legacy/include/* \
-        /usr/lib/kbd/keymaps/legacy/i386/qwerty/us.*; do
-            [[ -f "$i" ]] || continue
-            inst "$i"
-    done
+    local i p
+    local -a prefix=(
+        "/usr/lib"
+        "/usr/share"
+    )
 
-    # When it takes any argument, then install more keymaps.
-    if [[ $# -gt 1 ]]; then
-        for i in \
-        /usr/lib/kbd/keymaps/i386/*/* \
-        /usr/lib/kbd/keymaps/legacy/i386/*/*; do
-            [[ -f "$i" ]] || continue
-            inst "$i"
+    dinfo "Install console keymaps"
+
+    if command -v meson >/dev/null \
+            && [[ "$(meson configure "${BUILD_DIR:?}" | grep 'split-usr' | awk '{ print $2 }')" == "true" ]] \
+            || [[ ! -L /lib ]]; then
+        prefix+=(
+            "/lib"
+        )
+    fi
+
+    if (( $# == 0 )); then
+        for p in "${prefix[@]}"; do
+            # The first three paths may be deprecated.
+            # It seems now the last three paths are used by many distributions.
+            for i in \
+                "$p"/kbd/keymaps/include/* \
+                "$p"/kbd/keymaps/i386/include/* \
+                "$p"/kbd/keymaps/i386/qwerty/us.* \
+                "$p"/kbd/keymaps/legacy/include/* \
+                "$p"/kbd/keymaps/legacy/i386/qwerty/us.* \
+                "$p"/kbd/keymaps/xkb/us*; do
+                    [[ -f "$i" ]] || continue
+                    inst "$i"
+            done
+        done
+    else
+        # When it takes any argument, then install more keymaps.
+        for p in "${prefix[@]}"; do
+            for i in \
+                "$p"/kbd/keymaps/include/* \
+                "$p"/kbd/keymaps/i386/*/* \
+                "$p"/kbd/keymaps/legacy/i386/*/* \
+                "$p"/kbd/keymaps/xkb/*; do
+                    [[ -f "$i" ]] || continue
+                    inst "$i"
+            done
         done
     fi
 }
 
+install_x11_keymaps() {
+    dinfo "Install x11 keymaps"
+
+    if (( $# == 0 )); then
+        # Install only keymap list.
+        inst /usr/share/X11/xkb/rules/base.lst
+    else
+        # When it takes any argument, then install all keymaps.
+        inst_recursive /usr/share/X11/xkb
+    fi
+}
+
 install_zoneinfo() {
     dinfo "Install time zones"
     inst_any /usr/share/zoneinfo/Asia/Seoul
@@ -1622,7 +2110,7 @@ install_terminfo() {
     for terminfodir in /lib/terminfo /etc/terminfo /usr/share/terminfo; do
         [ -f "${terminfodir}/l/linux" ] && break
     done
-    dracut_install -o "${terminfodir}/l/linux"
+    image_install -o "${terminfodir}/l/linux"
 }
 
 has_user_dbus_socket() {
@@ -1634,6 +2122,8 @@ has_user_dbus_socket() {
     fi
 }
 
+setup_nspawn_root_hook() { :;}
+
 setup_nspawn_root() {
     if [ -z "${initdir}" ]; then
         dfatal "\$initdir not defined"
@@ -1646,6 +2136,8 @@ setup_nspawn_root() {
         ddebug "cp -ar $initdir $TESTDIR/unprivileged-nspawn-root"
         cp -ar "$initdir" "$TESTDIR/unprivileged-nspawn-root"
     fi
+
+    setup_nspawn_root_hook
 }
 
 setup_basic_dirs() {
@@ -1682,6 +2174,9 @@ inst_libs() {
 
     while read -r line; do
         [[ "$line" = 'not a dynamic executable' ]] && break
+        # Ignore errors about our own stuff missing. This is most likely caused
+        # by ldd attempting to use the unprefixed RPATH.
+        [[ "$line" =~ libsystemd.*\ not\ found ]] && continue
 
         if [[ "$line" =~ $so_regex ]]; then
             file="${BASH_REMATCH[1]}"
@@ -1694,7 +2189,7 @@ inst_libs() {
             dfatal "Missing a shared library required by $bin."
             dfatal "Run \"ldd $bin\" to find out what it is."
             dfatal "$line"
-            dfatal "dracut cannot create an initrd."
+            dfatal "Cannot create a test image."
             exit 1
         fi
     done < <(LC_ALL=C ldd "$bin" 2>/dev/null)
@@ -1730,6 +2225,24 @@ import_initdir() {
     export initdir
 }
 
+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
+            ;;
+        *)
+            dfatal "Failed to determine host's cgroup hierarchy"
+            exit 1
+    esac
+}
+
 ## @brief Converts numeric logging level to the first letter of level name.
 #
 # @param lvl Numeric logging level in range from 1 to 6.
@@ -1849,7 +2362,7 @@ dfatal() {
 
 
 # Generic substring function.  If $2 is in $1, return 0.
-strstr() { [ "${1#*$2*}" != "$1" ]; }
+strstr() { [ "${1#*"$2"*}" != "$1" ]; }
 
 # normalize_path <path>
 # Prints the normalized path, where it removes any duplicated
@@ -1888,8 +2401,7 @@ convert_abs_rel() {
     __abssize=${#__absolute[@]}
     __cursize=${#__current[@]}
 
-    while [[ "${__absolute[__level]}" == "${__current[__level]}" ]]
-    do
+    while [[ "${__absolute[__level]}" == "${__current[__level]}" ]]; do
         (( __level++ ))
         if (( __level > __abssize || __level > __cursize ))
         then
@@ -1897,8 +2409,7 @@ convert_abs_rel() {
         fi
     done
 
-    for ((__i = __level; __i < __cursize-1; __i++))
-    do
+    for ((__i = __level; __i < __cursize-1; __i++)); do
         if ((__i > __level))
         then
             __newpath=$__newpath"/"
@@ -1906,8 +2417,7 @@ convert_abs_rel() {
         __newpath=$__newpath".."
     done
 
-    for ((__i = __level; __i < __abssize; __i++))
-    do
+    for ((__i = __level; __i < __abssize; __i++)); do
         if [[ -n $__newpath ]]
         then
             __newpath=$__newpath"/"
@@ -2017,6 +2527,7 @@ inst_library() {
         inst_simple "$reallib" "$reallib"
         inst_dir "${dest%/*}"
         [[ -d "${dest%/*}" ]] && dest="$(readlink -f "${dest%/*}")/${dest##*/}"
+        ddebug "Creating symlink $reallib -> $dest"
         ln -sfn -- "$(convert_abs_rel "${dest}" "${reallib}")" "${initdir}/${dest}"
     else
         inst_simple "$src" "$dest"
@@ -2054,7 +2565,7 @@ inst_binary() {
     # In certain cases we might attempt to install a binary which is already
     # present in the test image, yet it's missing from the host system.
     # In such cases, let's check if the binary indeed exists in the image
-    # before doing any other chcecks. If it does, immediately return with
+    # before doing any other checks. If it does, immediately return with
     # success.
     if [[ $# -eq 1 ]]; then
         for path in "" bin sbin usr/bin usr/sbin; do
@@ -2069,10 +2580,23 @@ inst_binary() {
 
     local file line
     local so_regex='([^ ]*/lib[^/]*/[^ ]*\.so[^ ]*)'
+    # DSOs provided by systemd
+    local systemd_so_regex='/(libudev|libsystemd.*|.+[\-_]systemd([\-_].+)?|libnss_(mymachines|myhostname|resolve)).so'
+    local wrap_binary=0
     # I love bash!
     while read -r line; do
         [[ "$line" = 'not a dynamic executable' ]] && break
 
+        # Ignore errors about our own stuff missing. This is most likely caused
+        # by ldd attempting to use the unprefixed RPATH.
+        [[ "$line" =~ libsystemd.*\ not\ found ]] && continue
+
+        # We're built with ASan and the target binary loads one of the systemd's
+        # DSOs, so we need to tweak the environment before executing the binary
+        if get_bool "$IS_BUILT_WITH_ASAN" && [[ "$line" =~ $systemd_so_regex ]]; then
+            wrap_binary=1
+        fi
+
         if [[ "$line" =~ $so_regex ]]; then
             file="${BASH_REMATCH[1]}"
             [[ -e "${initdir}/$file" ]] && continue
@@ -2084,11 +2608,44 @@ inst_binary() {
             dfatal "Missing a shared library required by $bin."
             dfatal "Run \"ldd $bin\" to find out what it is."
             dfatal "$line"
-            dfatal "dracut cannot create an initrd."
+            dfatal "Cannot create a test image."
             exit 1
         fi
     done < <(LC_ALL=C ldd "$bin" 2>/dev/null)
-    inst_simple "$bin" "$target"
+
+    # Same as above, but we need to wrap certain libraries unconditionally
+    #
+    # chown, getent, login, su, useradd, userdel - dlopen()s (not only) systemd's PAM modules
+    # ls, stat - pulls in nss_systemd with certain options (like ls -l) when
+    #            nsswitch.conf uses [SUCCESS=merge] (like on Arch Linux)
+    # tar - called by machinectl in TEST-25
+    if get_bool "$IS_BUILT_WITH_ASAN" && [[ "$bin" =~ /(chown|getent|login|ls|stat|su|tar|useradd|userdel)$ ]]; then
+        wrap_binary=1
+    fi
+
+    # If the target binary is built with ASan support, we don't need to wrap
+    # it, as it should handle everything by itself
+    if get_bool "$wrap_binary" && ! is_built_with_asan "$bin"; then
+        dinfo "Creating ASan-compatible wrapper for binary '$target'"
+        # Install the target binary with a ".orig" suffix
+        inst_simple "$bin" "${target}.orig"
+        # Create a simple shell wrapper in place of the target binary, which
+        # sets necessary ASan-related env variables and then exec()s the
+        # suffixed target binary
+        cat >"$initdir/$target" <<EOF
+#!/bin/bash
+# Preload the ASan runtime DSO, otherwise ASAn will complain
+export LD_PRELOAD="$ASAN_RT_PATH"
+# Disable LSan to speed things up, since we don't care about leak reports
+# from 'external' binaries
+export ASAN_OPTIONS=detect_leaks=0
+# Set argv[0] to the original binary name without the ".orig" suffix
+exec -a "\$0" -- "${target}.orig" "\$@"
+EOF
+        chmod +x "$initdir/$target"
+    else
+        inst_simple "$bin" "$target"
+    fi
 }
 
 # same as above, except for shell scripts.
@@ -2144,7 +2701,7 @@ inst_rule_programs() {
         fi
 
         #dinfo "Installing $_bin due to it's use in the udev rule $(basename $1)"
-        dracut_install "$bin"
+        image_install "$bin"
     done
 }
 
@@ -2201,6 +2758,8 @@ inst() {
     for fun in inst_symlink inst_script inst_binary inst_simple; do
         "$fun" "$@" && return 0
     done
+
+    dwarn "Failed to install '$1'"
     return 1
 }
 
@@ -2216,7 +2775,7 @@ inst() {
 # inst_any -d /bin/foo /bin/bar /bin/baz
 #
 # Lets assume that /bin/baz exists, so it will be installed as /bin/foo in
-# initramfs.
+# initrd.
 inst_any() {
     local dest file
 
@@ -2232,10 +2791,28 @@ inst_any() {
     return 1
 }
 
-# dracut_install [-o ] <file> [<file> ... ]
-# Install <file> to the initramfs image
+inst_recursive() {
+    local p item
+
+    for p in "$@"; do
+        # Make sure the source exists, as the process substitution below
+        # suppresses errors
+        stat "$p" >/dev/null || return 1
+
+        while read -r item; do
+            if [[ -d "$item" ]]; then
+                inst_dir "$item"
+            elif [[ -f "$item" ]]; then
+                inst_simple "$item"
+            fi
+        done < <(find "$p" 2>/dev/null)
+    done
+}
+
+# image_install [-o ] <file> [<file> ... ]
+# Install <file> to the test image
 # -o optionally install the <file> and don't fail, if it is not there
-dracut_install() {
+image_install() {
     local optional=no
     local prog="${1:?}"
 
@@ -2262,12 +2839,12 @@ dracut_install() {
 install_kmod_with_fw() {
     local module="${1:?}"
     # no need to go further if the module is already installed
-    [[ -e "${initdir:?}/lib/modules/${KERNEL_VER:?}/${module##*/lib/modules/$KERNEL_VER/}" ]] && return 0
+    [[ -e "${initdir:?}/lib/modules/${KERNEL_VER:?}/${module##*"/lib/modules/$KERNEL_VER/"}" ]] && return 0
     [[ -e "$initdir/.kernelmodseen/${module##*/}" ]] && return 0
 
     [ -d "$initdir/.kernelmodseen" ] && : >"$initdir/.kernelmodseen/${module##*/}"
 
-    inst_simple "$module" "/lib/modules/$KERNEL_VER/${module##*/lib/modules/$KERNEL_VER/}" || return $?
+    inst_simple "$module" "/lib/modules/$KERNEL_VER/${module##*"/lib/modules/$KERNEL_VER/"}" || return $?
 
     local modname="${module##*/}"
     local fwdir found fw
@@ -2344,7 +2921,7 @@ instmods() {
                 else
                     (
                         [[ "$mpargs" ]] && echo "$mpargs"
-                        find "$mod_dir" -path "*/${mod#=}/*" -type f -printf '%f\n'
+                        find "$mod_dir" -path "*/${mod#=}/*" -name "*.ko*" -type f -printf '%f\n'
                     ) | instmods
                 fi
                 ;;
@@ -2400,12 +2977,6 @@ instmods() {
     return 0
 }
 
-setup_suse() {
-    ln -fs ../usr/bin/systemctl "${initdir:?}/bin/"
-    ln -fs ../usr/lib/systemd "$initdir/lib/"
-    inst_simple "/usr/lib/systemd/system/haveged.service"
-}
-
 _umount_dir() {
     local mountpoint="${1:?}"
     if mountpoint -q "$mountpoint"; then
@@ -2426,7 +2997,7 @@ _test_cleanup() {
         [[ -n "$initdir" ]] && _umount_dir "$initdir"
         [[ -n "$IMAGE_PUBLIC" ]] && rm -vf "$IMAGE_PUBLIC"
         # If multiple setups/cleans are ran in parallel, this can cause a race
-        if [[ -n "$IMAGESTATEDIR" &&  $TEST_PARALLELIZE -ne 1 ]]; then
+        if [[ -n "$IMAGESTATEDIR" && $TEST_PARALLELIZE -ne 1 ]]; then
             rm -vf "${IMAGESTATEDIR}/default.img"
         fi
         [[ -n "$TESTDIR" ]] && rm -vfr "$TESTDIR"
@@ -2500,13 +3071,24 @@ test_setup() {
         fi
 
         mount_initdir
-        # We want to test all services in TEST-01-BASIC, but mask them in
-        # all other tests
-        if [[ "${TESTID:?}" != "01" ]]; then
+
+        if get_bool "${TEST_SUPPORTING_SERVICES_SHOULD_BE_MASKED}"; then
             dinfo "Masking supporting services"
             mask_supporting_services
         fi
 
+        # Send stdout/stderr of testsuite-*.service units to both journal and
+        # console to make debugging in CIs easier
+        # Note: we can't use a dropin for `testsuite-.service`, since that also
+        #       overrides 'sub-units' of some tests that already use a specific
+        #       value for Standard(Output|Error)=
+        #       (e.g. test/units/testsuite-66-deviceisolation.service)
+        if ! get_bool "$INTERACTIVE_DEBUG"; then
+            local dropin_dir="${initdir:?}/etc/systemd/system/testsuite-${TESTID:?}.service.d"
+            mkdir -p "$dropin_dir"
+            printf '[Service]\nStandardOutput=journal+console\nStandardError=journal+console' >"$dropin_dir/99-stdout.conf"
+        fi
+
         if get_bool "$hook_defined"; then
             test_append_files "${initdir:?}"
         fi
@@ -2521,9 +3103,9 @@ test_run() {
 
     if ! get_bool "${TEST_NO_QEMU:=}"; then
         if run_qemu "$test_id"; then
-            check_result_qemu || { echo "QEMU test failed"; return 1; }
+            check_result_qemu || { echo "qemu test failed"; return 1; }
         else
-            dwarn "can't run QEMU, skipping"
+            dwarn "can't run qemu, skipping"
         fi
     fi
     if ! get_bool "${TEST_NO_NSPAWN:=}"; then
@@ -2553,12 +3135,12 @@ do_test() {
     fi
 
     if get_bool "${TEST_NO_QEMU:=}" && get_bool "${TEST_NO_NSPAWN:=}"; then
-        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: both QEMU and nspawn disabled" >&2
+        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: both qemu and nspawn disabled" >&2
         exit 0
     fi
 
     if get_bool "${TEST_QEMU_ONLY:=}" && ! get_bool "$TEST_NO_NSPAWN"; then
-        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: QEMU-only tests requested" >&2
+        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: qemu-only tests requested" >&2
         exit 0
     fi