summaryrefslogtreecommitdiffstats
path: root/debian/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 08:06:26 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 08:06:26 +0000
commitfd888e850cf413955483bfb993aeeea5ea611289 (patch)
tree6148fed3d1f30272c48403f4cdefa59c2b7e1513 /debian/tests
parentAdding upstream version 2:2.6.1. (diff)
downloadcryptsetup-debian.tar.xz
cryptsetup-debian.zip
Adding debian version 2:2.6.1-4~deb12u2.debian/2%2.6.1-4_deb12u2debian
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'debian/tests')
-rw-r--r--debian/tests/control133
-rwxr-xr-xdebian/tests/cryptdisks764
-rwxr-xr-xdebian/tests/cryptdisks.init84
l---------debian/tests/cryptroot-legacy1
-rw-r--r--debian/tests/cryptroot-legacy.d/bottom9
-rw-r--r--debian/tests/cryptroot-legacy.d/config14
-rwxr-xr-xdebian/tests/cryptroot-legacy.d/mock32
-rw-r--r--debian/tests/cryptroot-legacy.d/preinst14
-rw-r--r--debian/tests/cryptroot-legacy.d/setup46
l---------debian/tests/cryptroot-lvm1
-rw-r--r--debian/tests/cryptroot-lvm.d/bottom9
-rw-r--r--debian/tests/cryptroot-lvm.d/config10
-rwxr-xr-xdebian/tests/cryptroot-lvm.d/mock49
-rw-r--r--debian/tests/cryptroot-lvm.d/postinst17
-rw-r--r--debian/tests/cryptroot-lvm.d/preinst14
-rw-r--r--debian/tests/cryptroot-lvm.d/setup45
l---------debian/tests/cryptroot-md1
-rw-r--r--debian/tests/cryptroot-md.d/bottom15
-rw-r--r--debian/tests/cryptroot-md.d/config7
-rwxr-xr-xdebian/tests/cryptroot-md.d/mock41
-rw-r--r--debian/tests/cryptroot-md.d/preinst20
-rw-r--r--debian/tests/cryptroot-md.d/setup84
l---------debian/tests/cryptroot-nested1
-rw-r--r--debian/tests/cryptroot-nested.d/bottom17
-rw-r--r--debian/tests/cryptroot-nested.d/config7
-rwxr-xr-xdebian/tests/cryptroot-nested.d/mock44
-rw-r--r--debian/tests/cryptroot-nested.d/preinst21
-rw-r--r--debian/tests/cryptroot-nested.d/setup107
-rwxr-xr-xdebian/tests/cryptroot-run135
l---------debian/tests/cryptroot-sysvinit1
-rw-r--r--debian/tests/cryptroot-sysvinit.d/bottom9
-rw-r--r--debian/tests/cryptroot-sysvinit.d/config5
-rwxr-xr-xdebian/tests/cryptroot-sysvinit.d/mock31
-rw-r--r--debian/tests/cryptroot-sysvinit.d/postinst15
-rw-r--r--debian/tests/cryptroot-sysvinit.d/preinst16
-rw-r--r--debian/tests/cryptroot-sysvinit.d/setup43
-rwxr-xr-xdebian/tests/initramfs-hook267
-rwxr-xr-xdebian/tests/utils/cryptroot-common537
-rwxr-xr-xdebian/tests/utils/debootstrap125
-rwxr-xr-xdebian/tests/utils/init273
-rwxr-xr-xdebian/tests/utils/mkinitramfs159
-rw-r--r--debian/tests/utils/mock.pm347
42 files changed, 3570 insertions, 0 deletions
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644
index 0000000..52752a3
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,133 @@
+# Run the installed binaries and libraries through the full upstream test suite.
+Features: test-name=upstream-testsuite
+Test-Command: make -C ./tests -f Makefile.localtest -j tests CRYPTSETUP_PATH=/sbin TESTSUITE_NOSKIP=y
+Depends: cryptsetup-bin,
+# to compile tests/*.c
+ gcc,
+ libcryptsetup-dev,
+ libdevmapper-dev,
+#
+# for hexdump(1)
+ bsdextrautils,
+# for dmsetup(8)
+ dmsetup,
+# for expect(1)
+ expect,
+# for jq(1)
+ jq,
+# for keyctl(1)
+ keyutils,
+# for modprobe(8) and rmmod(8)
+ kmod,
+# for free(1)
+ procps,
+# for uuencode(1)
+ sharutils,
+# for xxd(1)
+ xxd
+#
+# Use machine-level isolation since some extra tests want to interact
+# with the kernel, load modules, and create/remove loop devices
+Restrictions: allow-stderr, needs-root, isolation-machine, rw-build-tree
+
+# Run ./tests/ssh-test-plugin on its own since it has its own dependency set.
+Features: test-name=ssh-test-plugin
+Test-Command: cd ./tests && CRYPTSETUP_PATH=/sbin TESTSUITE_NOSKIP=y RUN_SSH_PLUGIN_TEST=y ./ssh-test-plugin
+Depends: cryptsetup-bin,
+ cryptsetup-ssh,
+ netcat-openbsd,
+ openssh-client,
+ openssh-server,
+ openssl,
+ sshpass
+Restrictions: needs-root, isolation-machine
+
+
+Tests: cryptdisks, cryptdisks.init
+Depends: cryptsetup, xxd
+Restrictions: allow-stderr, needs-root, isolation-machine
+
+# This test doesn't replace the cryptroot-* tests below which mock a
+# complete system incl. unlocking at initramfs stage, but it's also
+# significantly faster so we use it for crude checks of our initramfs
+# hook and the initramfs image itself.
+Tests: initramfs-hook
+Depends: cryptsetup-initramfs, e2fsprogs, zstd
+Restrictions: allow-stderr, needs-root, isolation-machine
+
+Tests: cryptroot-lvm, cryptroot-legacy
+# Only dependencies required to set the VM here are listed here;
+# cryptsetup is not listed since we only install it in the VM.
+Depends: cryptsetup-bin,
+ dosfstools [arm64 armhf],
+ fdisk,
+ genext2fs,
+ initramfs-tools-core,
+ libjson-perl,
+ lvm2,
+ qemu-efi-aarch64 [arm64],
+ qemu-efi-arm [armhf],
+ qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system,
+ udev
+# We only need root to create /dev/kvm, really. And while it works
+# locally and on debci, it doesn't work on salsa CI..
+Restrictions: allow-stderr, needs-root
+Architecture: amd64 i386
+
+Tests: cryptroot-md
+Depends: cryptsetup-bin,
+ dosfstools [arm64 armhf],
+ fdisk,
+ genext2fs,
+ initramfs-tools-core,
+ libjson-perl,
+ lvm2,
+ mdadm,
+ qemu-efi-aarch64 [arm64],
+ qemu-efi-arm [armhf],
+ qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system,
+ udev
+Restrictions: allow-stderr, needs-root
+Architecture: amd64 i386
+
+Tests: cryptroot-nested
+Depends: btrfs-progs,
+ cryptsetup-bin,
+ dosfstools [arm64 armhf],
+ fdisk,
+ genext2fs,
+ initramfs-tools-core,
+ libjson-perl,
+ lvm2,
+ mdadm,
+ qemu-efi-aarch64 [arm64],
+ qemu-efi-arm [armhf],
+ qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system,
+ udev
+Restrictions: allow-stderr, needs-root
+Architecture: amd64 i386
+
+Tests: cryptroot-sysvinit
+Depends: cryptsetup-bin,
+ dosfstools [arm64 armhf],
+ fdisk,
+ genext2fs,
+ initramfs-tools-core,
+ libjson-perl,
+ qemu-efi-aarch64 [arm64],
+ qemu-efi-arm [armhf],
+ qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system,
+ udev
+Restrictions: allow-stderr, needs-root
+Architecture: amd64 i386
+
+# Dummy test so that kernel updates trigger our other autopkgtests on debci
+Features: test-name=hint-testsuite-triggers
+Test-Command: false
+Depends: linux-image-generic,
+ linux-image-amd64 [amd64],
+ linux-image-arm64 [arm64],
+ linux-image-armmp-lpae [armhf],
+ linux-image-686-pae [i386]
+Restrictions: hint-testsuite-triggers
+Architecture: amd64 i386
diff --git a/debian/tests/cryptdisks b/debian/tests/cryptdisks
new file mode 100755
index 0000000..3d3223b
--- /dev/null
+++ b/debian/tests/cryptdisks
@@ -0,0 +1,764 @@
+#!/bin/bash
+
+set -eux
+PATH="/usr/bin:/bin:/usr/sbin:/sbin"
+export PATH
+
+TMPDIR="$AUTOPKGTEST_TMP"
+
+# wrappers
+luks1Format() {
+ cryptsetup luksFormat --batch-mode --type=luks1 \
+ --pbkdf-force-iterations=1000 \
+ "$@"
+}
+luks2Format() {
+ cryptsetup luksFormat --batch-mode --type=luks2 \
+ --pbkdf=argon2id --pbkdf-force-iterations=4 --pbkdf-memory=32 \
+ "$@"
+}
+diff() { command diff --color=auto --text "$@"; }
+
+# create disk image
+CRYPT_IMG="$TMPDIR/disk.img"
+CRYPT_DEV=""
+install -m0600 /dev/null "$TMPDIR/keyfile"
+disk_setup() {
+ local lo
+ for lo in $(losetup -j "$CRYPT_IMG" | cut -sd: -f1); do
+ losetup -d "$lo"
+ done
+ dd if="/dev/zero" of="$CRYPT_IMG" bs=1M count=64
+ CRYPT_DEV="$(losetup --find --show -- "$CRYPT_IMG")"
+}
+
+
+#######################################################################
+# make sure empty passphrases are NEVER accepted
+
+disk_setup
+! cryptsetup luksFormat "$CRYPT_DEV" </dev/null || exit 1
+! blkid -p "$CRYPT_DEV" || exit 1
+
+! echo -n "" | cryptsetup luksFormat "$CRYPT_DEV" - || exit 1
+! blkid -p "$CRYPT_DEV" || exit 1
+
+! cryptsetup luksFormat --batch-mode "$CRYPT_DEV" /dev/null || exit 1
+! blkid -p "$CRYPT_DEV" || exit 1
+
+! cryptsetup luksFormat --batch-mode "$CRYPT_DEV" </dev/null || exit 1
+! blkid -p "$CRYPT_DEV" || exit 1
+
+! echo -n "" | luks2Format "$CRYPT_DEV" - || exit 1
+! blkid -p "$CRYPT_DEV" || exit 1
+
+
+#######################################################################
+# LUKS
+
+# interactive
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+t="$(blkid -s TYPE -o value -- "$CRYPT_DEV")"
+test "$t" = "crypto_LUKS"
+
+cat >/etc/crypttab <<-EOF
+ test0_crypt $CRYPT_DEV none
+EOF
+cryptdisks_start test0_crypt </dev/tty & pid=$!
+
+# check command line and environment
+until [ -p /lib/cryptsetup/passfifo ]; do sleep 1; done
+pid2="$(find /proc/[0-9]* -mindepth 1 -maxdepth 1 -name "exe" \
+ -execdir sh -euc 'diff -q -- "$0" /usr/lib/cryptsetup/askpass >/dev/null' {} \; \
+ -print 2>/dev/null | cut -sd/ -f3)"
+test -n "$pid2"
+printf '%s\0Please unlock disk %s: \0' /lib/cryptsetup/askpass test0_crypt >"$TMPDIR/cmdline"
+diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline" "/proc/$pid2/cmdline"
+tr '\n' '\0' >"$TMPDIR/environ" <<-EOF
+ CRYPTTAB_NAME=test0_crypt
+ CRYPTTAB_OPTIONS=
+ CRYPTTAB_SOURCE=$CRYPT_DEV
+ CRYPTTAB_TRIED=0
+ _CRYPTTAB_NAME=test0_crypt
+ _CRYPTTAB_OPTIONS=
+ _CRYPTTAB_SOURCE=$CRYPT_DEV
+EOF
+grep -Ez "^_?CRYPTTAB_" <"/proc/$pid2/environ" | sort -z | diff -u --label=a/environ --label=b/environ -- "$TMPDIR/environ" -
+
+# unlock device
+tr -d '\n' <"$TMPDIR/passphrase" >/lib/cryptsetup/passfifo # remove trailing newline
+wait $pid
+stty sane || true
+test -b /dev/mapper/test0_crypt
+
+# check default cipher (if it changes we probably want to update the doc and revise some scripts)
+cipher="$(dmsetup table --target=crypt test0_crypt | cut -d" " -f4)"
+test "$cipher" = "aes-xts-plain64"
+
+# make sure the kernel keyring is used by default for the encryption key
+key="$(dmsetup table --target=crypt test0_crypt | cut -d" " -f5)"
+test "${key:0:21}" = ":64:logon:cryptsetup:"
+
+cryptdisks_stop test0_crypt
+
+# remove trailing newline and unlock via key file
+tr -d '\n' <"$TMPDIR/passphrase" >"$TMPDIR/keyfile"
+cat >/etc/crypttab <<-EOF
+ test0_crypt $CRYPT_DEV $TMPDIR/keyfile
+EOF
+cryptdisks_start test0_crypt
+test -b /dev/mapper/test0_crypt
+cryptdisks_stop test0_crypt
+
+# special characters
+ln -sT -- keyfile "$TMPDIR/key fi:le"
+cat >/etc/crypttab <<-EOF
+ test0\\0045crypt $CRYPT_DEV $TMPDIR/key\\0040fi\\0072le
+EOF
+cryptdisks_start "test0%crypt"
+dmsetup table --target=crypt "test0%crypt" | cut -d" " -f5 | grep -F ":64:logon:cryptsetup:" # name in /dev/mapper is probably mangled
+cryptdisks_stop "test0%crypt"
+
+
+#######################################################################
+# cipher=, size= (plain)
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+cat >/etc/crypttab <<-EOF
+ plain_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=twofish-cbc-essiv:sha256,size=256
+EOF
+
+cryptdisks_start plain_crypt
+test -b /dev/mapper/plain_crypt
+
+# check cipher
+cipher="$(dmsetup table --target=crypt plain_crypt | cut -d" " -f4)"
+test "$cipher" = "twofish-cbc-essiv:sha256"
+
+# check encryption key
+xxd -ps -c256 "$TMPDIR/keyfile" >"$TMPDIR/keyfile-hex"
+dmsetup table --target=crypt --showkeys plain_crypt | cut -d" " -f5 | \
+ diff --label=a/key --label=b/key "$TMPDIR/keyfile-hex" -
+
+cryptdisks_stop plain_crypt
+
+
+#######################################################################
+# sector-size=
+
+disk_setup
+cat >/etc/crypttab <<-EOF
+ sector_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-cbc-essiv:sha256,size=256,sector-size=4096
+EOF
+
+cryptdisks_start sector_crypt
+test -b /dev/mapper/sector_crypt
+
+dmsetup table --target=crypt sector_crypt | cut -d" " -f10- | grep -Fw "sector_size:4096"
+
+cryptdisks_stop sector_crypt
+
+
+#######################################################################
+# hash= (interactive, ignored with keyfile)
+
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+cat >/etc/crypttab <<-EOF
+ hash_crypt $CRYPT_DEV none plain,cipher=aes-cbc-essiv:sha256,size=256,hash=sha256
+EOF
+
+cryptdisks_start hash_crypt </dev/tty & pid=$!
+until [ -p /lib/cryptsetup/passfifo ]; do sleep 1; done
+tr -d '\n' <"$TMPDIR/passphrase" >/lib/cryptsetup/passfifo # remove trailing newline
+wait $pid
+stty sane || true
+test -b /dev/mapper/hash_crypt
+
+# check encryption key
+tr -d '\n' <"$TMPDIR/passphrase" | sha256sum | cut -d" " -f1 >"$TMPDIR/passphrase-hash"
+dmsetup table --target=crypt --showkeys hash_crypt | cut -d" " -f5 | \
+ diff --label=a/key --label=b/key "$TMPDIR/passphrase-hash" -
+cryptdisks_stop hash_crypt
+
+
+#######################################################################
+# offset=, skip=
+
+offset=2048 # in 512 byte sectors
+skip=256 # in 512 byte sectors
+disk_setup
+cat >/etc/crypttab <<-EOF
+ offset_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-cbc-essiv:sha256,size=256,offset=$offset,skip=$skip
+EOF
+
+# having an existing file system before the offset has no effect (cf. #994056)
+dmsetup create hidden --table "0 $offset linear $CRYPT_DEV 0"
+mke2fs -t ext2 -m0 -Fq /dev/mapper/hidden
+u="$(blkid -p -s UUID -o value /dev/mapper/hidden)"
+dd if=/dev/mapper/hidden of="$TMPDIR/hidden.img" bs=512
+dmsetup remove hidden
+u2="$(blkid -p -s UUID -o value -- "$CRYPT_DEV")"
+test "$u" = "$u2"
+
+cryptdisks_start offset_crypt
+test -b /dev/mapper/offset_crypt
+
+# check offset and skip values
+offset2="$(dmsetup table --target=crypt offset_crypt | cut -d" " -f8)" && test $offset -eq $offset2
+skip2="$( dmsetup table --target=crypt offset_crypt | cut -d" " -f6)" && test $skip -eq $skip2
+
+# ensure that the first 2048 sectors (only) are left zeroed out
+dd if=/dev/zero of=/dev/mapper/offset_crypt bs=1M || true
+cryptdisks_stop offset_crypt
+
+dd if="$CRYPT_DEV" of="$TMPDIR/hidden2.img" bs=512 count="$offset"
+command diff -q -- "$TMPDIR/hidden.img" "$TMPDIR/hidden2.img" || exit 1
+! xxd -l32 -s$((offset*512)) -ps -c32 <"$CRYPT_DEV" | grep -Fxq 0000000000000000000000000000000000000000000000000000000000000000
+rm -f -- "$TMPDIR/hidden.img" "$TMPDIR/hidden2.img"
+
+
+#######################################################################
+# keyfile-offset=, keyfile-size=
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+install -m0600 /dev/null "$TMPDIR/keyfile2"
+
+# keyfile-offset=
+head -c1024 </dev/urandom >"$TMPDIR/keyfile2"
+cat "$TMPDIR/keyfile" >>"$TMPDIR/keyfile2"
+cat >/etc/crypttab <<-EOF
+ keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 keyfile-offset=1024
+EOF
+cryptdisks_start keyfile_crypt
+test -b /dev/mapper/keyfile_crypt
+cryptdisks_stop keyfile_crypt
+
+# keyfile-size=
+cat "$TMPDIR/keyfile" >"$TMPDIR/keyfile2"
+head -c1024 </dev/urandom >>"$TMPDIR/keyfile2"
+cat >/etc/crypttab <<-EOF
+ keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 keyfile-size=32
+EOF
+cryptdisks_start keyfile_crypt
+test -b /dev/mapper/keyfile_crypt
+cryptdisks_stop keyfile_crypt
+
+# keyfile-offset= + keyfile-size=
+head -c32 </dev/urandom >"$TMPDIR/keyfile2"
+cat "$TMPDIR/keyfile" >>"$TMPDIR/keyfile2"
+head -c32 </dev/urandom >>"$TMPDIR/keyfile2"
+cat >/etc/crypttab <<-EOF
+ keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 keyfile-offset=32,keyfile-size=32
+EOF
+cryptdisks_start keyfile_crypt
+test -b /dev/mapper/keyfile_crypt
+cryptdisks_stop keyfile_crypt
+
+# make sure the key isn't valid without offset and size
+cat >/etc/crypttab <<-EOF
+ keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2
+EOF
+! cryptdisks_start keyfile_crypt
+test ! -b /dev/mapper/keyfile_crypt
+rm -vf -- "$TMPDIR/keyfile2"
+
+
+#######################################################################
+# key-slot=
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format --key-slot=0 -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+install -m0600 /dev/null "$TMPDIR/keyfile2"
+head -c32 </dev/urandom >"$TMPDIR/keyfile2"
+cryptsetup luksAddKey --key-file="$TMPDIR/keyfile" \
+ --pbkdf=pbkdf2 --pbkdf-force-iterations=1000 \
+ --key-slot=1 -- "$CRYPT_DEV" "$TMPDIR/keyfile2"
+
+cryptsetup luksOpen --test-passphrase --key-file="$TMPDIR/keyfile" --key-slot=0 -- "$CRYPT_DEV"
+cryptsetup luksOpen --test-passphrase --key-file="$TMPDIR/keyfile2" --key-slot=1 -- "$CRYPT_DEV"
+
+# use slot #1 after trying #0
+cat >/etc/crypttab <<-EOF
+ keyslot_crypt $CRYPT_DEV $TMPDIR/keyfile2
+EOF
+cryptdisks_start keyslot_crypt
+test -b /dev/mapper/keyslot_crypt
+cryptdisks_stop keyslot_crypt
+
+# use wrong slot #0
+cat >/etc/crypttab <<-EOF
+ keyslot_crypt $CRYPT_DEV $TMPDIR/keyfile2 key-slot=0
+EOF
+! cryptdisks_start keyslot_crypt
+test ! -b /dev/mapper/keyslot_crypt
+
+# use right slot #1
+cat >/etc/crypttab <<-EOF
+ keyslot_crypt $CRYPT_DEV $TMPDIR/keyfile2 key-slot=1
+EOF
+cryptdisks_start keyslot_crypt
+test -b /dev/mapper/keyslot_crypt
+cryptdisks_stop keyslot_crypt
+rm -f -- "$TMPDIR/keyfile2"
+
+
+#######################################################################
+# header=
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format --header="$TMPDIR/crypt_img.hdr" -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+test -f "$TMPDIR/crypt_img.hdr"
+
+# make sure the signature is on the header only
+t="$(blkid -s TYPE -o value -- "$TMPDIR/crypt_img.hdr")"
+test "$t" = "crypto_LUKS"
+! blkid -p -- "$CRYPT_DEV"
+
+# make sure we can't unlock without the header
+cat >/etc/crypttab <<-EOF
+ header_crypt $CRYPT_DEV $TMPDIR/keyfile luks
+EOF
+! cryptdisks_start header_crypt
+test ! -b /dev/mapper/header_crypt
+
+# unlock using the header
+cat >/etc/crypttab <<-EOF
+ header_crypt $CRYPT_DEV $TMPDIR/keyfile header=$TMPDIR/crypt_img.hdr
+EOF
+cryptdisks_start header_crypt
+test -b /dev/mapper/header_crypt
+cryptdisks_stop header_crypt
+rm -f -- "$TMPDIR/crypt_img.hdr"
+
+
+#######################################################################
+# readonly
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+# unlock readonly from crypttab(5)
+cat >/etc/crypttab <<-EOF
+ readonly_crypt $CRYPT_DEV $TMPDIR/keyfile readonly
+EOF
+cryptdisks_start readonly_crypt
+test -b /dev/mapper/readonly_crypt
+dm="$(readlink -e "/dev/mapper/readonly_crypt")"
+ro="$(< "/sys/block/${dm##*/}/ro")"
+test "$ro" -eq 1
+cryptdisks_stop readonly_crypt
+
+# unlock readonly with --readonly
+cat >/etc/crypttab <<-EOF
+ readonly_crypt $CRYPT_DEV $TMPDIR/keyfile
+EOF
+cryptdisks_start --readonly readonly_crypt
+test -b /dev/mapper/readonly_crypt
+dm="$(readlink -e "/dev/mapper/readonly_crypt")"
+ro="$(< "/sys/block/${dm##*/}/ro")"
+test "$ro" -eq 1
+cryptdisks_stop readonly_crypt
+
+# double check that default is read-write
+cryptdisks_start readonly_crypt
+test -b /dev/mapper/readonly_crypt
+dm="$(readlink -e "/dev/mapper/readonly_crypt")"
+ro="$(< "/sys/block/${dm##*/}/ro")"
+test "$ro" -eq 0
+cryptdisks_stop readonly_crypt
+
+
+#######################################################################
+# tries=
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+# fail after 3 tries default
+cat >/etc/crypttab <<-EOF
+ tries_crypt $CRYPT_DEV none
+EOF
+
+cryptdisks_start tries_crypt </dev/tty & pid=$!
+echo -n bad1 >/lib/cryptsetup/passfifo
+sleep 1
+echo -n bad2 >/lib/cryptsetup/passfifo
+sleep 1
+echo -n bad3 >/lib/cryptsetup/passfifo
+! wait $pid
+stty sane || true
+test ! -b /dev/mapper/tries_crypt
+
+# success on the 3rd try
+cryptdisks_start tries_crypt </dev/tty & pid=$!
+echo -n bad1 >/lib/cryptsetup/passfifo
+sleep 1
+echo -n bad2 >/lib/cryptsetup/passfifo
+sleep 1
+cat <"$TMPDIR/keyfile" >/lib/cryptsetup/passfifo
+wait $pid
+stty sane || true
+test -b /dev/mapper/tries_crypt
+cryptdisks_stop tries_crypt
+
+# force single try
+cat >/etc/crypttab <<-EOF
+ tries_crypt $CRYPT_DEV none tries=1
+EOF
+
+cryptdisks_start tries_crypt </dev/tty & pid=$!
+echo -n bad1 >/lib/cryptsetup/passfifo
+! wait $pid
+stty sane || true
+test ! -b /dev/mapper/tries_crypt
+
+cryptdisks_start tries_crypt </dev/tty & pid=$!
+cat <"$TMPDIR/keyfile" >/lib/cryptsetup/passfifo
+wait $pid
+stty sane || true
+test -b /dev/mapper/tries_crypt
+cryptdisks_stop tries_crypt
+
+
+#######################################################################
+# discard
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+cat >/etc/crypttab <<-EOF
+ flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile discard
+EOF
+
+cryptdisks_start flagopt_crypt
+dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "allow_discards"
+cryptdisks_stop flagopt_crypt
+
+
+#######################################################################
+# same-cpu-crypt
+
+cat >/etc/crypttab <<-EOF
+ flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile same-cpu-crypt
+EOF
+
+cryptdisks_start flagopt_crypt
+dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "same_cpu_crypt"
+cryptdisks_stop flagopt_crypt
+
+
+#######################################################################
+# submit-from-crypt-cpus
+
+cat >/etc/crypttab <<-EOF
+ flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile submit-from-crypt-cpus
+EOF
+
+cryptdisks_start flagopt_crypt
+dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "submit_from_crypt_cpus"
+cryptdisks_stop flagopt_crypt
+
+
+#######################################################################
+# no-read-workqueue
+
+cat >/etc/crypttab <<-EOF
+ flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile no-read-workqueue
+EOF
+
+cryptdisks_start flagopt_crypt
+dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "no_read_workqueue"
+cryptdisks_stop flagopt_crypt
+
+
+#######################################################################
+# no-write-workqueue
+
+cat >/etc/crypttab <<-EOF
+ flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile no-write-workqueue
+EOF
+
+cryptdisks_start flagopt_crypt
+dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "no_write_workqueue"
+cryptdisks_stop flagopt_crypt
+
+
+#######################################################################
+# swap
+
+disk_setup
+cat >/etc/crypttab <<-EOF
+ swap_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,swap
+EOF
+
+cryptdisks_start swap_crypt
+test -b /dev/mapper/swap_crypt
+
+t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt)"
+test "$t" = "swap"
+cryptdisks_stop swap_crypt
+
+# refuse to proceed if the target contains a file system...
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+cat >/etc/crypttab <<-EOF
+ swap_crypt $CRYPT_DEV $TMPDIR/keyfile swap
+ swap_crypt2 $CRYPT_DEV $TMPDIR/keyfile
+EOF
+cryptdisks_start swap_crypt2
+mke2fs -t ext4 -m0 -Fq /dev/mapper/swap_crypt2
+t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt2)"
+test "$t" = "ext4"
+cryptdisks_stop swap_crypt2
+
+! cryptdisks_start swap_crypt
+test ! -b /dev/mapper/swap_crypt
+
+# ... unless that's already a swap device
+cryptdisks_start swap_crypt2
+mkswap -f /dev/mapper/swap_crypt2
+t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt2)"
+test "$t" = "swap"
+u="$(blkid -s UUID -o value /dev/mapper/swap_crypt2)"
+cryptdisks_stop swap_crypt2
+
+cryptdisks_start swap_crypt
+test -b /dev/mapper/swap_crypt
+t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt)"
+test "$t" = "swap"
+u2="$(blkid -s UUID -o value /dev/mapper/swap_crypt)"
+test "$u" != "$u2"
+cryptdisks_stop swap_crypt
+
+
+#######################################################################
+# tmp=
+
+disk_setup
+cat >/etc/crypttab <<-EOF
+ tmp_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,tmp=ext2
+EOF
+
+# run mkfs.ext2
+cryptdisks_start tmp_crypt
+test -b /dev/mapper/tmp_crypt
+
+t="$(blkid -s TYPE -o value /dev/mapper/tmp_crypt)"
+test "$t" = "ext2"
+cryptdisks_stop tmp_crypt
+
+# default type is ext4
+cat >/etc/crypttab <<-EOF
+ tmp_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,tmp
+EOF
+cryptdisks_start tmp_crypt
+t="$(blkid -s TYPE -o value /dev/mapper/tmp_crypt)"
+test "$t" = "ext4"
+cryptdisks_stop tmp_crypt
+
+
+#######################################################################
+# check=
+
+disk_setup
+cat >/etc/crypttab <<-EOF
+ check_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256
+EOF
+
+# precheck failed: $CRYPT_DEV contains a filesystem
+mke2fs -t ext4 -m0 -Fq -- "$CRYPT_DEV"
+t="$(blkid -s TYPE -o value -- "$CRYPT_DEV")"
+test "$t" = "ext4"
+! cryptdisks_start check_crypt
+test ! -b /dev/mapper/check_crypt
+
+# precheck failed: $CRYPT_DEV contains a filesystem at the given offset (cf. #994056)
+offset=2048
+disk_setup
+cat >/etc/crypttab <<-EOF
+ check_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,offset=$offset
+EOF
+
+dmsetup create hidden --table "0 4096 linear $CRYPT_DEV $offset"
+mke2fs -t ext2 -m0 -Fq /dev/mapper/hidden
+u="$(blkid -p -s UUID -o value /dev/mapper/hidden)"
+dmsetup remove hidden
+u2="$(blkid -p -O$((offset*512)) -s UUID -o value -- "$CRYPT_DEV")"
+test "$u" = "$u2"
+t="$(blkid -p -O$((offset*512)) -s TYPE -o value -- "$CRYPT_DEV")"
+test "$t" = "ext2"
+
+! cryptdisks_start check_crypt
+test ! -b /dev/mapper/check_crypt
+
+# check failed: mapped device does not contain a known file system
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+cat >/etc/crypttab <<-EOF
+ check_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check
+ check_crypt2 $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256
+EOF
+
+! cryptdisks_start check_crypt
+test ! -b /dev/mapper/check_crypt
+
+# success
+cryptdisks_start check_crypt2
+mke2fs -t ext4 -m0 -Fq /dev/mapper/check_crypt2
+u="$(blkid -s UUID -o value /dev/mapper/check_crypt2)"
+cryptdisks_stop check_crypt2
+cryptdisks_start check_crypt
+test -b /dev/mapper/check_crypt
+u2="$(blkid -s UUID -o value /dev/mapper/check_crypt)"
+test "$u" = "$u2"
+cryptdisks_stop check_crypt
+
+# custom check
+install -m0755 -- /dev/null "$TMPDIR/check"
+cat >"$TMPDIR/check" <<-EOF
+ #!/bin/bash
+ printf '%s\\0' "\$0" >"$TMPDIR/cmdline"
+ while [ \$# -gt 0 ]; do
+ printf '%s\\0' "\$1"
+ shift
+ done >>"$TMPDIR/cmdline"
+ exit 0
+EOF
+
+cat >/etc/crypttab <<-EOF
+ check_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check=$TMPDIR/check
+EOF
+cryptdisks_start check_crypt
+dm="$(readlink -e "/dev/mapper/check_crypt")"
+cryptdisks_stop check_crypt
+printf '%s\0%s\0' "$TMPDIR/check" "$dm" >"$TMPDIR/cmdline2"
+diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline2" "$TMPDIR/cmdline"
+
+
+#######################################################################
+# checkargs=
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+cat >/etc/crypttab <<-EOF
+ checkargs_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check,checkargs=ext4
+ checkargs_crypt2 $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256
+EOF
+
+# check failed: mapped device does not contain a known file system
+! cryptdisks_start checkargs_crypt
+test ! -b /dev/mapper/checkargs_crypt
+
+# check failed: mapped device is not ext4
+cryptdisks_start checkargs_crypt2
+mke2fs -t ext2 -m0 -Fq /dev/mapper/checkargs_crypt2
+cryptdisks_stop checkargs_crypt2
+! cryptdisks_start checkargs_crypt
+test ! -b /dev/mapper/checkargs_crypt
+
+# success
+cryptdisks_start checkargs_crypt2
+mke2fs -t ext4 -m0 -Fq /dev/mapper/checkargs_crypt2
+u="$(blkid -s UUID -o value /dev/mapper/checkargs_crypt2)"
+cryptdisks_stop checkargs_crypt2
+cryptdisks_start checkargs_crypt
+u2="$(blkid -s UUID -o value /dev/mapper/checkargs_crypt)"
+test "$u" = "$u2"
+test -b /dev/mapper/checkargs_crypt
+cryptdisks_stop checkargs_crypt
+
+# check failed: mapped device is not ext2
+sed -i "s/checkargs=ext4/checkargs=ext2/" /etc/crypttab
+! cryptdisks_start checkargs_crypt
+test ! -b /dev/mapper/checkargs_crypt
+
+# custom check
+cat >/etc/crypttab <<-EOF
+ checkargs_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check=$TMPDIR/check,checkargs=foo\\0012b\\0011a\\0054r\\0040
+EOF
+cryptdisks_start checkargs_crypt
+dm="$(readlink -e "/dev/mapper/checkargs_crypt")"
+cryptdisks_stop checkargs_crypt
+printf '%s\0%s\0foo\nb\ta,r \0' "$TMPDIR/check" "$dm" >"$TMPDIR/cmdline2"
+diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline2" "$TMPDIR/cmdline"
+
+
+#######################################################################
+# noauto
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+cat >/etc/crypttab <<-EOF
+ noauto_crypt $CRYPT_DEV $TMPDIR/keyfile noauto
+EOF
+cryptdisks_start noauto_crypt
+test -b /dev/mapper/noauto_crypt
+cryptdisks_stop noauto_crypt
+
+
+#######################################################################
+# (custom) keyscript
+
+disk_setup
+head -c32 </dev/urandom >"$TMPDIR/keyfile"
+luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile"
+
+KEYSCRIPT="$TMPDIR/decrypt_foo,bar
+b a z"
+
+# make sure we export CRYPTTAB_* as documented
+install -m0755 -- /dev/null "$KEYSCRIPT"
+cat >"$KEYSCRIPT" <<-EOF
+ #!/bin/bash
+ printf '%s\\0' "\$0" >"$TMPDIR/cmdline"
+ while [ \$# -gt 0 ]; do
+ printf '%s\\0' "\$1"
+ shift
+ done >>"$TMPDIR/cmdline"
+ install -m0600 "/proc/\$\$/environ" "$TMPDIR/environ"
+ cat <"$TMPDIR/keyfile"
+EOF
+
+# add extra unknown option (visible in $CRYPTTAB_OPTIONS but there is no $CRYPTTAB_OPTION_*)
+cat >/etc/crypttab <<-EOF
+ keyscript\\0045crypt $CRYPT_IMG foo\\0011bar\\0040baz nonexistent,keyscript=$TMPDIR/decrypt_foo\\0054bar\\0012b\\0040a\\0040z,luks
+EOF
+
+cryptdisks_start "keyscript%crypt"
+dmsetup table --target=crypt "keyscript%crypt" | cut -d" " -f5 | grep -F ":64:logon:cryptsetup:" # name in /dev/mapper is probably mangled
+cryptdisks_stop "keyscript%crypt"
+
+# compare command line
+printf '%s\0foo\tbar baz\0' "$KEYSCRIPT" >"$TMPDIR/cmdline2"
+diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline2" "$TMPDIR/cmdline"
+
+# compare environment
+tr '\n' '\0' <<-EOF | sed -rz "s|@@DECRYPT_FOOBAR@@|${KEYSCRIPT//$'\n'/"\\n"}|" >"$TMPDIR/environ2"
+ CRYPTTAB_KEY=foo bar baz
+ CRYPTTAB_NAME=keyscript%crypt
+ CRYPTTAB_OPTIONS=nonexistent,keyscript=@@DECRYPT_FOOBAR@@,luks
+ CRYPTTAB_OPTION_keyscript=@@DECRYPT_FOOBAR@@
+ CRYPTTAB_OPTION_luks=yes
+ CRYPTTAB_SOURCE=$CRYPT_IMG
+ CRYPTTAB_TRIED=0
+ _CRYPTTAB_KEY=foo\\0011bar\\0040baz
+ _CRYPTTAB_NAME=keyscript\\0045crypt
+ _CRYPTTAB_OPTIONS=nonexistent,keyscript=$TMPDIR/decrypt_foo\\0054bar\\0012b\\0040a\\0040z,luks
+ _CRYPTTAB_SOURCE=$CRYPT_IMG
+EOF
+grep -Ez "^_?CRYPTTAB_" <"$TMPDIR/environ" | sort -z | diff -u --label=a/environ --label=b/environ -- "$TMPDIR/environ2" -
diff --git a/debian/tests/cryptdisks.init b/debian/tests/cryptdisks.init
new file mode 100755
index 0000000..408c325
--- /dev/null
+++ b/debian/tests/cryptdisks.init
@@ -0,0 +1,84 @@
+#!/bin/bash
+
+set -eu
+PATH="/usr/bin:/bin:/usr/sbin:/sbin"
+export PATH
+
+if [ -d /run/systemd/system ]; then
+ export SYSTEMCTL_SKIP_REDIRECT="y"
+ # systemd masks cryptdisks.service and we can't unmask it because /etc/init.d is the only source
+ rm -f -- $(systemctl show -p FragmentPath --value cryptdisks.service)
+ systemctl daemon-reload
+fi
+
+# create 64M zero devices
+dmsetup create disk0 --table "0 $(( 64 * 2*1024)) zero"
+dmsetup create disk1 --table "0 $(( 64 * 2*1024)) zero"
+dmsetup create disk2 --table "0 $(( 64 * 2*1024)) zero"
+dmsetup create disk3 --table "0 $((128 * 2*1024)) zero"
+
+# join disk #1 and #2
+dmsetup create disk12 <<-EOF
+ 0 $((64 * 2*1024)) linear /dev/mapper/disk1 0
+ $((64 * 2*1024)) $((64 * 2*1024)) linear /dev/mapper/disk2 0
+EOF
+
+cipher="aes-cbc-essiv:sha256"
+size=32 # bytes
+cat >/etc/crypttab <<-EOF
+ crypt_disk0 /dev/mapper/disk0 /dev/urandom plain,cipher=$cipher,size=$((8*size))
+ crypt_disk0a /dev/mapper/crypt_disk0 /dev/urandom plain,cipher=$cipher,size=$((8*size))
+ crypt_disk12 /dev/mapper/disk12 /dev/urandom plain,cipher=$cipher,size=$((8*size))
+ crypt_disk3 /dev/mapper/disk3 /dev/urandom plain,cipher=$cipher,size=$((8*size))
+ crypt_disk3b /dev/mapper/crypt_disk3 /dev/urandom plain,cipher=$cipher,size=$((8*size)),offset=$(( 64 * 2*1024))
+ crypt_disk3b0 /dev/mapper/crypt_disk3b /dev/urandom plain,cipher=$cipher,size=$((8*size))
+EOF
+
+/etc/init.d/cryptdisks start
+
+# now add crypt_disk3a (preceeding crypt_disk3b) with a size limit (can't do that via crypttab but dmsetup allows it)
+dmsetup create crypt_disk3a --uuid "CRYPT-PLAIN-crypt_disk3a" --addnodeoncreate <<-EOF
+ 0 $((64 * 2*1024)) crypt $cipher $(xxd -l$size -ps -c256 </dev/urandom) 0 /dev/mapper/crypt_disk3 0
+EOF
+
+lsblk
+# disk0 253:0 0 64M 0 dm
+# └─crypt_disk0 253:5 0 64M 0 crypt
+# └─crypt_disk0a 253:6 0 64M 0 crypt
+# disk1 253:1 0 64M 0 dm
+# └─disk12 253:4 0 128M 0 dm
+# └─crypt_disk12 253:7 0 128M 0 crypt
+# disk2 253:2 0 64M 0 dm
+# └─disk12 253:4 0 128M 0 dm
+# └─crypt_disk12 253:7 0 128M 0 crypt
+#disk3 253:3 0 128M 0 dm
+#└─crypt_disk3 253:8 0 128M 0 crypt
+# ├─crypt_disk3b 253:9 0 64M 0 crypt
+# │ └─crypt_disk3b0 253:10 0 64M 0 crypt
+# └─crypt_disk3a 253:11 0 64M 0 dm
+
+# check device-mapper table (crypt target only)
+# https://gitlab.com/cryptsetup/cryptsetup/-/wikis/DMCrypt
+# <start_sector> <size> "crypt" <target mapping table> <cipher> <key> <iv_offset> <device path> <offset> [<#opt_params> <opt_params>]
+dmsetup table --target="crypt" >"$AUTOPKGTEST_TMP/table"
+sed -ri "s/\\s+0{$((2*size))}(\\s+[0-9]+)\\s+[0-9]+:[0-9]+(\s|$)/\\1\\2/" -- "$AUTOPKGTEST_TMP/table"
+LC_ALL=C sort -t: -k1,1 <"$AUTOPKGTEST_TMP/table" >"$AUTOPKGTEST_TMP/table2"
+
+diff -u --color=auto --label="a/table" --label="b/table" -- - "$AUTOPKGTEST_TMP/table2" <<-EOF
+ crypt_disk0: 0 $((64 * 2*1024)) crypt $cipher 0 0
+ crypt_disk0a: 0 $((64 * 2*1024)) crypt $cipher 0 0
+ crypt_disk12: 0 $((2*64 * 2*1024)) crypt $cipher 0 0
+ crypt_disk3: 0 $((128 * 2*1024)) crypt $cipher 0 0
+ crypt_disk3a: 0 $((64 * 2*1024)) crypt $cipher 0 0
+ crypt_disk3b: 0 $((64 * 2*1024)) crypt $cipher 0 $((64 * 2*1024))
+ crypt_disk3b0: 0 $((64 * 2*1024)) crypt $cipher 0 0
+EOF
+
+# close disks and ensure there no leftover devices
+/etc/init.d/cryptdisks stop
+dmsetup table --target="crypt" >"$AUTOPKGTEST_TMP/table"
+if [ -s "$AUTOPKGTEST_TMP/table" ]; then
+ echo "ERROR: leftover crypt devices" >&2
+ cat <"$AUTOPKGTEST_TMP/table"
+ exit 1
+fi
diff --git a/debian/tests/cryptroot-legacy b/debian/tests/cryptroot-legacy
new file mode 120000
index 0000000..2e34c2d
--- /dev/null
+++ b/debian/tests/cryptroot-legacy
@@ -0,0 +1 @@
+utils/cryptroot-common \ No newline at end of file
diff --git a/debian/tests/cryptroot-legacy.d/bottom b/debian/tests/cryptroot-legacy.d/bottom
new file mode 100644
index 0000000..8bf492f
--- /dev/null
+++ b/debian/tests/cryptroot-legacy.d/bottom
@@ -0,0 +1,9 @@
+umount "$ROOT/boot"
+umount "$ROOT"
+
+swapoff /dev/cryptvg/swap
+lvm vgchange -an "cryptvg"
+
+cryptsetup close "vda3_crypt"
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-legacy.d/config b/debian/tests/cryptroot-legacy.d/config
new file mode 100644
index 0000000..cff461c
--- /dev/null
+++ b/debian/tests/cryptroot-legacy.d/config
@@ -0,0 +1,14 @@
+PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4
+PKGS_EXTRA+=( lvm2 )
+PKGS_EXTRA+=( cryptsetup-initramfs )
+
+# disable AES and SHA instructions
+if [[ "$QEMU_CPU_MODEL" =~ ^(.*),\+aes(,.*)?$ ]]; then
+ QEMU_CPU_MODEL="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
+fi
+if [[ "$QEMU_CPU_MODEL" =~ ^(.*),\+sha-ni(,.*)?$ ]]; then
+ QEMU_CPU_MODEL="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
+fi
+QEMU_CPU_MODEL="$QEMU_CPU_MODEL,-aes,-sha-ni"
+
+# vim: set filetype=bash :
diff --git a/debian/tests/cryptroot-legacy.d/mock b/debian/tests/cryptroot-legacy.d/mock
new file mode 100755
index 0000000..b3b7d26
--- /dev/null
+++ b/debian/tests/cryptroot-legacy.d/mock
@@ -0,0 +1,32 @@
+#!/usr/bin/perl -T
+
+BEGIN {
+ require "./debian/tests/utils/mock.pm";
+ CryptrootTest::Mock::->import();
+}
+
+unlock_disk("topsecret");
+login("root");
+
+# make sure the root FS and swap are help by dm-crypt devices
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vda3 <<<topsecret}, rv => 0);
+my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3});
+die unless $out =~ m#^`-vda3_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^\s{2}[`|]-cryptvg-root\s+lvm\s+/\s*$#m;
+die unless $out =~ m#^\s{2}[`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m;
+
+# assume MODULES=dep won't add too many modules
+# XXX lsinitramfs doesn't work on /initrd.img with COMPRESS=zstd, cf. #1015954
+$out = shell(q{lsinitramfs /boot/initrd.img-`uname -r` | grep -Ec "^(usr/)?lib/modules/.*\.ko(\.[a-z]+)?$"});
+die "$out == 0 or $out > 50" unless $out =~ s/\r?\n\z// and $out =~ /\A([0-9]+)\z/ and $out > 0 and $out <= 50;
+
+# check cipher and key size
+$out = shell(q{dmsetup table --target crypt --showkeys vda3_crypt});
+die unless $out =~ m#\A0\s+\d+\s+crypt\s+aes-cbc-essiv:sha256\s+[0-9a-f]{64}\s#;
+
+# make sure hardware acceleration for AES isn't available
+$out = shell(q{cat /proc/crypto});
+die unless $out =~ m#^name\s*:.*\baes\b#mi;
+die if $out =~ m#^(?:name|driver)\s*:.*\b__(?:.*\b)?aes\b#mi;
+
+QMP::quit();
diff --git a/debian/tests/cryptroot-legacy.d/preinst b/debian/tests/cryptroot-legacy.d/preinst
new file mode 100644
index 0000000..ee76481
--- /dev/null
+++ b/debian/tests/cryptroot-legacy.d/preinst
@@ -0,0 +1,14 @@
+cat >/etc/crypttab <<-EOF
+ vda3_crypt UUID=$(blkid -s UUID -o value /dev/vda3) none luks,discard
+EOF
+
+cat >/etc/fstab <<-EOF
+ /dev/cryptvg/root / auto errors=remount-ro 0 1
+ /dev/cryptvg/swap none swap sw 0 0
+ UUID=$(blkid -s UUID -o value /dev/vda2) /boot auto defaults 0 2
+EOF
+
+# explicitely set MODULES=dep (yes it's the default, but doesn't hurt)
+echo "MODULES=dep" >/etc/initramfs-tools/conf.d/modules
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-legacy.d/setup b/debian/tests/cryptroot-legacy.d/setup
new file mode 100644
index 0000000..c7ab31f
--- /dev/null
+++ b/debian/tests/cryptroot-legacy.d/setup
@@ -0,0 +1,46 @@
+# LVM-on-LUKS2 layout from an old system: pre-2013 cryptsetup defaults,
+# no AES hardware acceleration (and MODULES=dep)
+
+sfdisk --append /dev/vda <<-EOF
+ unit: sectors
+
+ start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS}
+ start=$(((64+128)*1024*2)), type=${GUID_TYPE_LUKS}
+EOF
+udevadm settle
+
+# Use pre-2013 (<1.6.0) defaults: LUKS1, aes-cbc-essiv:sha256 cipher, 256bits key
+# <1.6.0 default hash was sha1 but we use legacy hash ripemd160 here to test OpenSSL's
+# legacy.so
+echo -n "topsecret" >/rootfs.key
+cryptsetup luksFormat --batch-mode \
+ --key-file=/rootfs.key \
+ --type=luks1 \
+ --pbkdf-force-iterations=1000 \
+ --cipher="aes-cbc-essiv:sha256" \
+ --hash="ripemd160" \
+ --key-size=256 \
+ -- /dev/vda3
+cryptsetup luksOpen --key-file=/rootfs.key --allow-discards \
+ -- /dev/vda3 "vda3_crypt"
+udevadm settle
+
+lvm pvcreate /dev/mapper/vda3_crypt
+lvm vgcreate "cryptvg" /dev/mapper/vda3_crypt
+lvm lvcreate -Zn --size 64m --name "swap" "cryptvg"
+lvm lvcreate -Zn -l100%FREE --name "root" "cryptvg"
+lvm vgchange -ay "cryptvg"
+lvm vgmknodes
+udevadm settle
+
+mke2fs -Ft ext4 /dev/cryptvg/root
+mount -t ext4 /dev/cryptvg/root "$ROOT"
+
+mkdir "$ROOT/boot"
+mke2fs -Ft ext2 -m0 /dev/vda2
+mount -t ext2 /dev/vda2 "$ROOT/boot"
+
+mkswap /dev/cryptvg/swap
+swapon /dev/cryptvg/swap
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-lvm b/debian/tests/cryptroot-lvm
new file mode 120000
index 0000000..2e34c2d
--- /dev/null
+++ b/debian/tests/cryptroot-lvm
@@ -0,0 +1 @@
+utils/cryptroot-common \ No newline at end of file
diff --git a/debian/tests/cryptroot-lvm.d/bottom b/debian/tests/cryptroot-lvm.d/bottom
new file mode 100644
index 0000000..8bf492f
--- /dev/null
+++ b/debian/tests/cryptroot-lvm.d/bottom
@@ -0,0 +1,9 @@
+umount "$ROOT/boot"
+umount "$ROOT"
+
+swapoff /dev/cryptvg/swap
+lvm vgchange -an "cryptvg"
+
+cryptsetup close "vda3_crypt"
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-lvm.d/config b/debian/tests/cryptroot-lvm.d/config
new file mode 100644
index 0000000..ac595b0
--- /dev/null
+++ b/debian/tests/cryptroot-lvm.d/config
@@ -0,0 +1,10 @@
+PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4
+PKGS_EXTRA+=( dbus ) # for systemctl(1)
+PKGS_EXTRA+=( lvm2 )
+PKGS_EXTRA+=( cryptsetup-initramfs cryptsetup-suspend )
+
+QEMU_MEMORY="size=512M"
+GUEST_POWERCYCLE=1 # boot again after hibernation
+DRIVE_SIZES=( "3G" ) # need a big enough swap to accomodate the memory
+
+# vim: set filetype=bash :
diff --git a/debian/tests/cryptroot-lvm.d/mock b/debian/tests/cryptroot-lvm.d/mock
new file mode 100755
index 0000000..f57e42f
--- /dev/null
+++ b/debian/tests/cryptroot-lvm.d/mock
@@ -0,0 +1,49 @@
+#!/usr/bin/perl -T
+
+BEGIN {
+ require "./debian/tests/utils/mock.pm";
+ CryptrootTest::Mock::->import();
+}
+
+my $POWERCYCLE_COUNT = $ARGV[0];
+
+unlock_disk("topsecret");
+
+if ($POWERCYCLE_COUNT == 0) {
+ login("root");
+
+ # make sure the root FS and swap are help by dm-crypt devices
+ shell(q{cryptsetup luksOpen --test-passphrase /dev/vda3 <<<topsecret}, rv => 0);
+ my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3});
+ die unless $out =~ m#^`-vda3_crypt\s+crypt\s*$#m;
+ die unless $out =~ m#^\s{2}[`|]-cryptvg-root\s+lvm\s+/\s*$#m;
+ die unless $out =~ m#^\s{2}[`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m;
+
+ # create a stamp in memory, hibernate (suspend on disk) and thaw
+ shell(q{echo hello >/dev/shm/foo.stamp});
+ hibernate();
+}
+else {
+ expect($SERIAL => qr/(?:^|\s)?PM: (?:hibernation: )?hibernation exit\r\n/m);
+ # no need to relogin, we get the shell as we left it
+ shell(q{grep -Fx hello </dev/shm/foo.stamp}, rv => 0);
+
+ # briefly suspend
+ suspend();
+
+ # make sure wakeup yields a cryptsetup prompt
+ wakeup();
+ expect($SERIAL => qr/(?:^|\s)?PM: suspend exit\r\n/m);
+ unlock_disk("topsecret");
+
+ # consume PS1 to make sure we're at a shell prompt
+ expect($CONSOLE => qr/\A $PS1 \z/aamsx);
+ my $out = shell(q{dmsetup info -c --noheadings -omangled_name,suspended --separator ' '});
+ die if grep !/[:[:blank:]]Active$/i, split(/\r?\n/, $out);
+
+ # test I/O on the root file system
+ shell(q{cp -vT /dev/shm/foo.stamp /cryptroot.stamp});
+ shell(q{grep -Fx hello </cryptroot.stamp}, rv => 0);
+
+ QMP::quit();
+}
diff --git a/debian/tests/cryptroot-lvm.d/postinst b/debian/tests/cryptroot-lvm.d/postinst
new file mode 100644
index 0000000..b9ffe35
--- /dev/null
+++ b/debian/tests/cryptroot-lvm.d/postinst
@@ -0,0 +1,17 @@
+mkdir /etc/systemd/system/systemd-suspend.service.d
+cat >/etc/systemd/system/systemd-suspend.service.d/zz-cryptsetup-suspend-mock.conf <<-EOF
+ # override the command and don't call openvt(1) here since VT8 isn't
+ # available from the mocking logic -- we use /dev/console instead
+
+ [Service]
+ StandardInput=tty
+ StandardOutput=inherit
+ StandardError=inherit
+ TTYPath=/dev/console
+ TTYReset=yes
+
+ ExecStart=
+ ExecStart=/lib/cryptsetup/scripts/suspend/cryptsetup-suspend-wrapper
+EOF
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-lvm.d/preinst b/debian/tests/cryptroot-lvm.d/preinst
new file mode 100644
index 0000000..650b9b6
--- /dev/null
+++ b/debian/tests/cryptroot-lvm.d/preinst
@@ -0,0 +1,14 @@
+cat >/etc/crypttab <<-EOF
+ vda3_crypt PARTUUID=$(blkid -s PARTUUID -o value /dev/vda3) none luks,discard
+EOF
+
+cat >/etc/fstab <<-EOF
+ /dev/cryptvg/root / auto errors=remount-ro 0 1
+ /dev/cryptvg/swap none swap sw 0 0
+ UUID=$(blkid -s UUID -o value /dev/vda2) /boot auto defaults 0 2
+EOF
+
+mkdir -p /etc/initramfs-tools/conf.d
+echo "RESUME=/dev/cryptvg/swap" >/etc/initramfs-tools/conf.d/resume
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-lvm.d/setup b/debian/tests/cryptroot-lvm.d/setup
new file mode 100644
index 0000000..890bbb6
--- /dev/null
+++ b/debian/tests/cryptroot-lvm.d/setup
@@ -0,0 +1,45 @@
+# Simple LVM-on-LUKS2 layout -- more or less emulates what one gets out
+# of d-i with the "encrypted LVM" partioning method.
+
+# create two new partitions for /boot and LUKS respectively (the first
+# one is always used for BIOS/EFI and never exceeds sector 64*1024*2)
+sfdisk --append /dev/vda <<-EOF
+ unit: sectors
+
+ start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS}
+ start=$(((64+128)*1024*2)), type=${GUID_TYPE_LUKS}
+EOF
+udevadm settle
+
+# initialize a new LUKS partition and open it
+echo -n "topsecret" >/rootfs.key
+cryptsetup luksFormat --batch-mode \
+ --key-file=/rootfs.key \
+ --type=luks2 \
+ --pbkdf=argon2id \
+ --pbkdf-force-iterations=4 \
+ --pbkdf-memory=32 \
+ -- /dev/vda3
+cryptsetup luksOpen --key-file=/rootfs.key --allow-discards \
+ -- /dev/vda3 "vda3_crypt"
+udevadm settle
+
+lvm pvcreate /dev/mapper/vda3_crypt
+lvm vgcreate "cryptvg" /dev/mapper/vda3_crypt
+lvm lvcreate -Zn --size 1024m --name "swap" "cryptvg"
+lvm lvcreate -Zn -l100%FREE --name "root" "cryptvg"
+lvm vgchange -ay "cryptvg"
+lvm vgmknodes
+udevadm settle
+
+mke2fs -Ft ext4 /dev/cryptvg/root
+mount -t ext4 /dev/cryptvg/root "$ROOT"
+
+mkdir "$ROOT/boot"
+mke2fs -Ft ext2 -m0 /dev/vda2
+mount -t ext2 /dev/vda2 "$ROOT/boot"
+
+mkswap /dev/cryptvg/swap
+swapon /dev/cryptvg/swap
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-md b/debian/tests/cryptroot-md
new file mode 120000
index 0000000..2e34c2d
--- /dev/null
+++ b/debian/tests/cryptroot-md
@@ -0,0 +1 @@
+utils/cryptroot-common \ No newline at end of file
diff --git a/debian/tests/cryptroot-md.d/bottom b/debian/tests/cryptroot-md.d/bottom
new file mode 100644
index 0000000..a771c91
--- /dev/null
+++ b/debian/tests/cryptroot-md.d/bottom
@@ -0,0 +1,15 @@
+umount "$ROOT/boot"
+umount "$ROOT"
+
+swapoff /dev/md1
+mdadm --stop /dev/md1
+cryptsetup close "vda3_crypt"
+cryptsetup close "vdb3_crypt"
+
+swapoff /dev/cryptvg/swap
+lvm vgchange -an "cryptvg"
+mdadm --stop /dev/md2
+cryptsetup close "vda4_crypt"
+cryptsetup close "vdb4_crypt"
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-md.d/config b/debian/tests/cryptroot-md.d/config
new file mode 100644
index 0000000..0c9e5ff
--- /dev/null
+++ b/debian/tests/cryptroot-md.d/config
@@ -0,0 +1,7 @@
+PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4
+PKGS_EXTRA+=( lvm2 mdadm )
+PKGS_EXTRA+=( cryptsetup-initramfs )
+
+DRIVE_SIZES=( "1536M" "1536M" )
+
+# vim: set filetype=bash :
diff --git a/debian/tests/cryptroot-md.d/mock b/debian/tests/cryptroot-md.d/mock
new file mode 100755
index 0000000..51f8c9c
--- /dev/null
+++ b/debian/tests/cryptroot-md.d/mock
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -T
+
+BEGIN {
+ require "./debian/tests/utils/mock.pm";
+ CryptrootTest::Mock::->import();
+}
+
+my %passphrases;
+$passphrases{$_} = $_ foreach qw/vda3_crypt vda4_crypt vdb3_crypt vdb4_crypt/;
+unlock_disk(\%passphrases) for 1 .. scalar(%passphrases);
+
+# check that the above was done at initramfs stage
+expect($SERIAL => qr#\bRunning /scripts/init-bottom\s*\.\.\. #);
+
+login("root");
+
+# make sure the root FS and swap are help by dm-crypt devices
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vda3 <<<vda3_crypt}, rv => 0);
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vda4 <<<vda4_crypt}, rv => 0);
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vdb3 <<<vdb3_crypt}, rv => 0);
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vdb4 <<<vdb4_crypt}, rv => 0);
+
+my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3});
+die unless $out =~ m#^`-vda3_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^ `-md1\s+raid0\s+\[SWAP\]\s*$#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdb3});
+die unless $out =~ m#^`-vdb3_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^ `-md1\s+raid0\s+\[SWAP\]\s*$#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda4});
+die unless $out =~ m#^`-vda4_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^ [`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m;
+die unless $out =~ m#^ [`|]-cryptvg-root\s+lvm\s+/\s*$#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdb4});
+die unless $out =~ m#^`-vdb4_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^ [`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m;
+die unless $out =~ m#^ [`|]-cryptvg-root\s+lvm\s+/\s*$#m;
+
+QMP::quit();
diff --git a/debian/tests/cryptroot-md.d/preinst b/debian/tests/cryptroot-md.d/preinst
new file mode 100644
index 0000000..84bfa7a
--- /dev/null
+++ b/debian/tests/cryptroot-md.d/preinst
@@ -0,0 +1,20 @@
+# intentionally mix UUID= and /dev
+cat >/etc/crypttab <<-EOF
+ vda3_crypt UUID=$(blkid -s UUID -o value /dev/vda3) none discard
+ vda4_crypt UUID=$(blkid -s UUID -o value /dev/vda4) none discard
+ vdb3_crypt /dev/vdb3 none discard
+ vdb4_crypt /dev/vdb4 none discard
+EOF
+
+cat >/etc/fstab <<-EOF
+ /dev/cryptvg/root / auto errors=remount-ro 0 1
+ /dev/cryptvg/swap none swap sw 0 0
+ /dev/md1 none swap sw 0 0
+ UUID=$(blkid -s UUID -o value /dev/md0) /boot auto defaults 0 2
+EOF
+
+# force unlocking /dev/md1 holders (/dev/vd[ab]3) at initramfs stage
+mkdir -p /etc/initramfs-tools/conf.d
+echo "RESUME=/dev/md1" >/etc/initramfs-tools/conf.d/resume
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-md.d/setup b/debian/tests/cryptroot-md.d/setup
new file mode 100644
index 0000000..a8f49ed
--- /dev/null
+++ b/debian/tests/cryptroot-md.d/setup
@@ -0,0 +1,84 @@
+# Rather convoluted LVM-on-MD-on-LUKS2 layout with 2 swap areas, /boot
+# on RAID1, SWAP0 on RAID0, LVM on RAID1 and 4 independently encrypted
+# partitions decrypt at early boot stage:
+
+# NAME TYPE MOUNTPOINTS
+# vda disk
+# ├─vda1 part
+# ├─vda2 part
+# │ └─md0 raid1 /boot
+# ├─vda3 part
+# │ └─vda3_crypt crypt
+# │ └─md1 raid0 [SWAP]
+# └─vda4 part
+# └─vda4_crypt crypt
+# └─md2 raid1
+# ├─cryptvg-swap lvm [SWAP]
+# └─cryptvg-root lvm /
+# vdb disk
+# ├─vdb1 part
+# ├─vdb2 part
+# │ └─md0 raid1 /boot
+# ├─vdb3 part
+# │ └─vdb3_crypt crypt
+# │ └─md1 raid0 [SWAP]
+# └─vdb4 part
+# └─vdb4_crypt crypt
+# └─md2 raid1
+# ├─cryptvg-swap lvm [SWAP]
+# └─cryptvg-root lvm /
+
+sfdisk --append /dev/vda <<-EOF
+ unit: sectors
+
+ start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS}
+ start=$(((64+128)*1024*2)), size=$((64*1024*2)), type=${GUID_TYPE_LUKS}
+ start=$(((64+128+64)*1024*2)), type=${GUID_TYPE_LUKS}
+EOF
+udevadm settle
+
+# copy vda's partition table onto vdb
+sfdisk -d /dev/vda | sfdisk /dev/vdb
+udevadm settle
+
+for d in vda3 vda4 vdb3 vdb4; do
+ echo -n "${d}_crypt" >/keyfile
+ cryptsetup luksFormat --batch-mode \
+ --key-file=/keyfile \
+ --type=luks2 \
+ --pbkdf=argon2id \
+ --pbkdf-force-iterations=4 \
+ --pbkdf-memory=32 \
+ -- "/dev/$d"
+ cryptsetup luksOpen --key-file=/keyfile --allow-discards \
+ -- "/dev/$d" "${d}_crypt"
+ udevadm settle
+done
+
+mdadm --create /dev/md0 --metadata=default --level=1 --raid-devices=2 /dev/vda2 /dev/vdb2
+mdadm --create /dev/md1 --metadata=default --level=0 --raid-devices=2 /dev/mapper/vda3_crypt /dev/mapper/vdb3_crypt
+mdadm --create /dev/md2 --metadata=default --level=1 --raid-devices=2 /dev/mapper/vda4_crypt /dev/mapper/vdb4_crypt
+udevadm settle
+
+lvm pvcreate /dev/md2
+lvm vgcreate "cryptvg" /dev/md2
+lvm lvcreate -Zn --size 64m --name "swap" "cryptvg"
+lvm lvcreate -Zn -l100%FREE --name "root" "cryptvg"
+lvm vgchange -ay "cryptvg"
+lvm vgmknodes
+udevadm settle
+
+
+mke2fs -Ft ext4 /dev/cryptvg/root
+mount -t ext4 /dev/cryptvg/root "$ROOT"
+
+mkdir "$ROOT/boot"
+mke2fs -Ft ext2 -m0 /dev/md0
+mount -t ext2 /dev/md0 "$ROOT/boot"
+
+mkswap /dev/cryptvg/swap
+swapon /dev/cryptvg/swap
+mkswap /dev/md1
+swapon /dev/md1
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-nested b/debian/tests/cryptroot-nested
new file mode 120000
index 0000000..2e34c2d
--- /dev/null
+++ b/debian/tests/cryptroot-nested
@@ -0,0 +1 @@
+utils/cryptroot-common \ No newline at end of file
diff --git a/debian/tests/cryptroot-nested.d/bottom b/debian/tests/cryptroot-nested.d/bottom
new file mode 100644
index 0000000..9c2e07a
--- /dev/null
+++ b/debian/tests/cryptroot-nested.d/bottom
@@ -0,0 +1,17 @@
+umount "$ROOT/boot"
+umount "$ROOT/home"
+umount "$ROOT/usr"
+umount "$ROOT/var"
+umount "$ROOT"
+
+swapoff /dev/mapper/testvg-lv0_crypt
+cryptsetup close "testvg-lv0_crypt"
+cryptsetup close "vdd_crypt"
+
+cryptsetup close "md0_crypt"
+mdadm --stop /dev/md0
+
+cryptsetup close "testvg-lv1_crypt"
+lvm vgchange -an "testvg"
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-nested.d/config b/debian/tests/cryptroot-nested.d/config
new file mode 100644
index 0000000..995200c
--- /dev/null
+++ b/debian/tests/cryptroot-nested.d/config
@@ -0,0 +1,7 @@
+PKGS_EXTRA+=( btrfs-progs lvm2 mdadm )
+PKGS_EXTRA+=( cryptsetup-initramfs )
+
+# /dev/mapper/testvg-lv1_crypt and /dev/vdc are both 1G and used in RAID1 mode
+DRIVE_SIZES=( "1G" "264M" "1G" "512M" )
+
+# vim: set filetype=bash :
diff --git a/debian/tests/cryptroot-nested.d/mock b/debian/tests/cryptroot-nested.d/mock
new file mode 100755
index 0000000..cccb35f
--- /dev/null
+++ b/debian/tests/cryptroot-nested.d/mock
@@ -0,0 +1,44 @@
+#!/usr/bin/perl -T
+
+BEGIN {
+ require "./debian/tests/utils/mock.pm";
+ CryptrootTest::Mock::->import();
+}
+
+my %passphrases;
+$passphrases{$_} = $_ foreach qw/testvg-lv0_crypt testvg-lv1_crypt md0_crypt vdd_crypt/;
+unlock_disk(\%passphrases) for 1 .. scalar(%passphrases);
+
+# check that the above was done at initramfs stage
+expect($SERIAL => qr#\bRunning /scripts/init-bottom\s*\.\.\. #);
+
+login("root");
+
+# make sure the root FS and swap are help by dm-crypt devices
+shell(q{cryptsetup luksOpen --test-passphrase /dev/md0 <<<md0_crypt}, rv => 0);
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vdd <<<vdd_crypt}, rv => 0);
+shell(q{cryptsetup luksOpen --test-passphrase /dev/testvg/lv1 <<<testvg-lv1_crypt}, rv => 0);
+
+my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3});
+die unless $out =~ m#^[`|]-testvg-lv0\s+lvm\s*$#m;
+die unless $out =~ m#^[| ] `-testvg-lv0_crypt\s+crypt\s+\[SWAP\]\s*$#m;
+die unless $out =~ m#^[`|]-testvg-lv1\s+lvm\s*$#m;
+die unless $out =~ m#^[| ] `-testvg-lv1_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^[| ] `-md0\s+raid1\s*$#m;
+die unless $out =~ m#^[| ] `-md0_crypt\s+crypt(?:\s+/(?:home|usr|var)?)?\s*$#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdb});
+die unless $out =~ m#^`-testvg-lv1\s+lvm\s*$#m;
+die unless $out =~ m#^ `-testvg-lv1_crypt\s+crypt\s*$#m;
+die unless $out =~ m#^ `-md0\s+raid1\s*$#m;
+die unless $out =~ m#^ `-md0_crypt\s+crypt(?:\s+/(?:home|usr|var)?)?\s*$#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdc});
+die unless $out =~ m#^`-md0\s+raid1\s*$#m;
+die unless $out =~ m#^ `-md0_crypt\s+crypt(?:\s+/(?:home|usr|var)?)?\s*$#m;
+
+$out = shell(q{btrfs filesystem show /});
+die unless $out =~ m#^\s*devid\s+1\s.*\s/dev/mapper/vdd_crypt\s*$#m;
+die unless $out =~ m#^\s*devid\s+2\s.*\s/dev/mapper/md0_crypt\s*$#m;
+
+QMP::quit();
diff --git a/debian/tests/cryptroot-nested.d/preinst b/debian/tests/cryptroot-nested.d/preinst
new file mode 100644
index 0000000..c5f576b
--- /dev/null
+++ b/debian/tests/cryptroot-nested.d/preinst
@@ -0,0 +1,21 @@
+# check both UUID= and /dev/mapper/NAME sources for testvg-*_crypt to test for regressions a la #902943
+cat >/etc/crypttab <<-EOF
+ md0_crypt UUID=$(blkid -s UUID -o value /dev/md0) none
+ vdd_crypt UUID=$(blkid -s UUID -o value /dev/vdd) none
+ testvg-lv0_crypt /dev/mapper/testvg-lv0 none plain,cipher=aes-cbc-essiv:sha256,size=256,hash=ripemd160
+ testvg-lv1_crypt UUID=$(blkid -s UUID -o value /dev/testvg/lv1) none
+EOF
+
+cat >/etc/fstab <<-EOF
+ /dev/mapper/vdd_crypt / btrfs compress=lzo,subvol=@ 0 1
+ /dev/mapper/vdd_crypt /home btrfs compress=lzo,subvol=@home 0 2
+ /dev/mapper/vdd_crypt /usr btrfs compress=lzo,subvol=@usr 0 2
+ /dev/mapper/vdd_crypt /var btrfs compress=lzo,subvol=@var 0 2
+ UUID=$(blkid -s UUID -o value /dev/vda2) /boot ext2 defaults 0 2
+ /dev/mapper/testvg-lv0_crypt none swap sw 0 0
+EOF
+
+mkdir -p /etc/initramfs-tools/conf.d
+echo "RESUME=/dev/mapper/testvg-lv0_crypt" >/etc/initramfs-tools/conf.d/resume
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-nested.d/setup b/debian/tests/cryptroot-nested.d/setup
new file mode 100644
index 0000000..6fb6ccd
--- /dev/null
+++ b/debian/tests/cryptroot-nested.d/setup
@@ -0,0 +1,107 @@
+# Unrealistic (and frankly stupid) layout with a complex block device
+# stack involving multi-device btrfs and btrfs subvolumes, LUKS-on-MD,
+# MD-on-LUKS and LUKS-on-LVM incl. nested dm-crypt volumes:
+
+# NAME TYPE MOUNTPOINTS
+# vda disk
+# ├─vda1 part
+# ├─vda2 part /boot
+# └─vda3 part
+# ├─testvg-lv0 lvm
+# │ └─testvg-lv0_crypt crypt [SWAP]
+# └─testvg-lv1 lvm
+# └─testvg-lv1_crypt crypt
+# └─md0 raid1
+# └─md0_crypt crypt /, /home, /usr, /var
+# vdb disk
+# └─testvg-lv1 lvm
+# └─testvg-lv1_crypt crypt
+# └─md0 raid1
+# └─md0_crypt crypt /, /home, /usr, /var
+# vdc disk
+# └─md0 raid1
+# └─md0_crypt crypt /, /home, /usr, /var
+# vdd disk
+# └─vdd_crypt crypt /, /home, /usr, /var
+
+sfdisk --append /dev/vda <<-EOF
+ unit: sectors
+
+ start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS}
+ start=$(((64+128)*1024*2)), type=${GUID_TYPE_LUKS}
+EOF
+udevadm settle
+
+lvm pvcreate /dev/vda3
+lvm pvcreate /dev/vdb
+lvm vgcreate "testvg" /dev/vda3 /dev/vdb
+lvm lvcreate -Zn --size 64m --name "lv0" "testvg"
+lvm lvcreate -Zn --size 1024m --name "lv1" "testvg"
+lvm vgchange -ay "testvg"
+lvm vgmknodes
+udevadm settle
+
+echo -n "testvg-lv0_crypt" >/keyfile
+cryptsetup open --batch-mode \
+ --type=plain \
+ --cipher="aes-cbc-essiv:sha256" \
+ --key-size=256 \
+ --hash="ripemd160" \
+ -- "/dev/testvg/lv0" "testvg-lv0_crypt" </keyfile
+udevadm settle
+
+echo -n "testvg-lv1_crypt" >/keyfile
+cryptsetup luksFormat --batch-mode \
+ --key-file=/keyfile \
+ --type=luks1 \
+ --pbkdf-force-iterations=1000 \
+ -- "/dev/testvg/lv1"
+cryptsetup luksOpen --key-file=/keyfile --allow-discards \
+ -- "/dev/testvg/lv1" "testvg-lv1_crypt"
+udevadm settle
+
+mdadm --create /dev/md0 --metadata=default --level=1 --raid-devices=2 \
+ /dev/mapper/testvg-lv1_crypt /dev/vdc
+udevadm settle
+
+for d in md0 vdd; do
+ echo -n "${d}_crypt" >/keyfile
+ cryptsetup luksFormat --batch-mode \
+ --key-file=/keyfile \
+ --type=luks2 \
+ --pbkdf=argon2id \
+ --pbkdf-force-iterations=4 \
+ --pbkdf-memory=32 \
+ -- "/dev/$d"
+ cryptsetup luksOpen --key-file=/keyfile --allow-discards \
+ -- "/dev/${d}" "${d}_crypt"
+ udevadm settle
+done
+
+# create multi-device btrfs filesystem for the root FS; we list /dev/mapper/vdd_crypt
+# first since it's smaller and we want data to span across both devices
+mkfs.btrfs -d single /dev/mapper/vdd_crypt /dev/mapper/md0_crypt
+
+# create subvolumes
+mount -t btrfs -o compress=lzo,device=/dev/mapper/md0_crypt /dev/mapper/vdd_crypt "$ROOT"
+btrfs subvol create "$ROOT/@"
+btrfs subvol create "$ROOT/@usr"
+btrfs subvol create "$ROOT/@var"
+btrfs subvol create "$ROOT/@home"
+umount "$ROOT"
+
+# now mount the subvolumes
+mount -t btrfs -o compress=lzo,device=/dev/mapper/md0_crypt,subvol="@" /dev/mapper/vdd_crypt "$ROOT"
+for s in home usr var; do
+ mkdir -m0755 "$ROOT/$s"
+ mount -t btrfs -o compress=lzo,device=/dev/mapper/md0_crypt,subvol="@$s" /dev/mapper/vdd_crypt "$ROOT/$s"
+done
+
+mkdir "$ROOT/boot"
+mke2fs -Ft ext2 -m0 /dev/vda2
+mount -t ext2 /dev/vda2 "$ROOT/boot"
+
+mkswap /dev/mapper/testvg-lv0_crypt
+swapon /dev/mapper/testvg-lv0_crypt
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-run b/debian/tests/cryptroot-run
new file mode 100755
index 0000000..6656bca
--- /dev/null
+++ b/debian/tests/cryptroot-run
@@ -0,0 +1,135 @@
+#!/bin/bash
+
+# Wrapper for cryptroot-* DEP-8 tests (outside autopkgtest harness)
+# This is mostly useful for local tests on the maintainers' machine,
+# such as expensive tests we don't want to overload debci with.
+#
+# Usage: d/t/cryptroot-run [TESTNAME ..]
+#
+# Copyright © 2022 Guilhem Moulin <guilhem@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -ue
+PATH="/usr/bin:/bin"
+export PATH
+
+if [ -n "${AUTOPKGTEST_TMP+x}" ]; then
+ echo "ERROR: This script is a test wrapper not an autopkgtest" >&2
+ exit 1
+fi
+
+# git-buildpackages's 'export-dir' option (XXX hardcoding this is not ideal)
+EXPORT_DIR="${XDG_CACHE_HOME:-"$HOME/.cache"}/build-area"
+
+RV=0
+TESTDIR="$(dirname -- "$0")"
+declare -a TESTNAMES=() TIME=() CODE=()
+
+# determine path to the .changes file and extract .deb file list from it
+DEB_VERSION="$(dpkg-parsechangelog -SVersion)"
+DEB_SOURCE="$(dpkg-parsechangelog -SSource)"
+DEB_BUILD_ARCHITECTURE="$(dpkg-architecture -qDEB_BUILD_ARCH)"
+if [[ "$DEB_VERSION" =~ ^[0-9]+:(.+)$ ]]; then
+ DEB_VERSION_NOEPOCH="${BASH_REMATCH[1]}"
+else
+ DEB_VERSION_NOEPOCH="$DEB_VERSION"
+fi
+
+CHANGES_FILE="${DEB_SOURCE}_${DEB_VERSION_NOEPOCH}_${DEB_BUILD_ARCHITECTURE}.changes"
+PKG_DIR="$(mktemp --tmpdir --directory "$DEB_SOURCE.XXXXXXXXXX")"
+trap "rm -rf -- \"$PKG_DIR\"" EXIT INT TERM
+
+if [ ! -f "$EXPORT_DIR/$CHANGES_FILE" ]; then
+ echo "ERROR: $EXPORT_DIR/$CHANGES_FILE: No such file" >&2
+ exit 1
+elif grep -qFxe "-----BEGIN PGP SIGNED MESSAGE-----" <"$EXPORT_DIR/$CHANGES_FILE"; then
+ gpgv --keyring=/dev/null --output="$PKG_DIR/$CHANGES_FILE" <"$EXPORT_DIR/$CHANGES_FILE" 2>/dev/null || true
+else
+ cp -T -- "$EXPORT_DIR/$CHANGES_FILE" "$PKG_DIR/$CHANGES_FILE"
+fi
+
+declare -a EXTRA_PKGS
+EXTRA_PKGS=( $(sed -nr '/^Files:/I {:l;n; /^\S/q; s/^\s.*\s(\S+\.deb)$/\1/p; b l }' "$PKG_DIR/$CHANGES_FILE") )
+if [ ${#EXTRA_PKGS[@]} -eq 0 ]; then
+ echo "ERROR: Couldn't extract .deb list from $CHANGES_FILE" >&2
+ exit 1
+fi
+
+# create temporary repository to expose locally-built .deb to cryptroot-* tests
+for deb in "${EXTRA_PKGS[@]}"; do
+ ln -st "$PKG_DIR" -- "$EXPORT_DIR/$deb" || exit 1
+done
+
+( cd "$PKG_DIR" && apt-ftparchive packages . >./Packages && apt-ftparchive release . >./Release )
+EXTRA_REPO="deb file:$PKG_DIR /"
+
+runtest() {
+ local rv=0 ts_start ts_stop
+ if [ -f "$t" ] && [ -d "$t.d" ]; then
+ t="${t#"$TESTDIR/"}"
+ echo ">>> Running $t..."
+ ts_start="$(printf "%(%s)T")"
+ "$TESTDIR/$t" "$EXTRA_REPO" </dev/null || rv=$?
+ ts_stop="$(printf "%(%s)T")"
+
+ if [ $rv -ne 0 ] && [ $RV -eq 0 -o $rv -lt $RV ]; then
+ RV=$rv
+ fi
+
+ TESTNAMES+=( "$t" )
+ TIME+=( $((ts_stop - ts_start)) )
+ CODE+=( $rv )
+ fi
+}
+
+
+if [ $# -eq 0 ]; then
+ for t in "$TESTDIR"/cryptroot-*; do
+ runtest "$t"
+ done
+else
+ for t in "$@"; do
+ if [ "${t#*/}" = "$t" ]; then
+ t="$TESTDIR/cryptroot-${t#cryptroot-}"
+ fi
+ runtest "$t"
+ done
+fi
+
+# show summary with test exit codes and elapsed time
+echo ==============================================================================
+print_sgr() {
+ local n="$1" msg="$2" fmt
+ [ -t 1 ] && fmt="\\x1B[${n}m%s\\x1B[0m" || fmt="%s"
+ printf " $fmt" "$msg"
+}
+for (( i = 0; i < ${#TESTNAMES[@]}; i++ )); do
+ printf "%s" "${TESTNAMES[i]}"
+ if [ ${CODE[i]} -eq 0 ]; then
+ print_sgr "1;32" "PASSED"
+ elif [ ${CODE[i]} -eq 77 ]; then
+ print_sgr "1;36" "SKIPPED"
+ elif [ ${CODE[i]} -eq 124 ]; then
+ print_sgr "1;31" "FAILED"
+ printf " (timeout)"
+ else
+ print_sgr "1;31" "FAILED"
+ printf " (with status %d)" ${CODE[i]}
+ fi
+ printf " after %d seconds\\n" ${TIME[i]}
+done
+echo ==============================================================================
+
+exit $RV
diff --git a/debian/tests/cryptroot-sysvinit b/debian/tests/cryptroot-sysvinit
new file mode 120000
index 0000000..2e34c2d
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit
@@ -0,0 +1 @@
+utils/cryptroot-common \ No newline at end of file
diff --git a/debian/tests/cryptroot-sysvinit.d/bottom b/debian/tests/cryptroot-sysvinit.d/bottom
new file mode 100644
index 0000000..13d5190
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit.d/bottom
@@ -0,0 +1,9 @@
+umount "$ROOT/boot"
+umount "$ROOT"
+
+swapoff /dev/mapper/vda4_crypt
+
+cryptsetup close "vda4_crypt"
+cryptsetup close "vda5_crypt"
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-sysvinit.d/config b/debian/tests/cryptroot-sysvinit.d/config
new file mode 100644
index 0000000..f6b7392
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit.d/config
@@ -0,0 +1,5 @@
+PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4
+PKGS_EXTRA+=( cryptsetup-initramfs cryptsetup )
+PKG_INIT="sysvinit-core"
+
+# vim: set filetype=bash :
diff --git a/debian/tests/cryptroot-sysvinit.d/mock b/debian/tests/cryptroot-sysvinit.d/mock
new file mode 100755
index 0000000..b729022
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit.d/mock
@@ -0,0 +1,31 @@
+#!/usr/bin/perl -T
+
+BEGIN {
+ require "./debian/tests/utils/mock.pm";
+ CryptrootTest::Mock::->import();
+}
+
+unlock_disk("topsecret");
+login("root");
+
+# make sure the root FS, swap, and /home are help by dm-crypt devices
+shell(q{cryptsetup luksOpen --test-passphrase /dev/vda5 <<<topsecret}, rv => 0);
+my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3});
+die unless $out =~ m#\Avda3\s.*\r?\n^`-vda3_crypt\s+crypt\s+/home\s*\r?\n\z#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda4});
+die unless $out =~ m#\Avda4\s.*\r?\n^`-vda4_crypt\s+crypt\s+\[SWAP\]\s*\r?\n\z#m;
+
+$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda5});
+die unless $out =~ m#\Avda5\s.*\r?\n^`-vda5_crypt\s+crypt\s+/\s*\r?\n\z#m;
+
+# make sure only vda5 is processed at initramfs stage
+# XXX unmkinitramfs doesn't work on /initrd.img with COMPRESS=zstd, cf. #1015954
+shell(q{unmkinitramfs /boot/initrd.img-`uname -r` /tmp/initramfs});
+shell(q{grep -E '^vd\S+_crypt\s' </tmp/initramfs/cryptroot/crypttab >/tmp/out});
+shell(q{grep -E '^vda5_crypt\s' </tmp/out}, rv => 0);
+shell(q{grep -Ev '^vda5_crypt\s' </tmp/out}, rv => 1);
+
+# don't use QMP::quit() here since we want to run our init scripts in
+# shutdown phase
+poweroff();
diff --git a/debian/tests/cryptroot-sysvinit.d/postinst b/debian/tests/cryptroot-sysvinit.d/postinst
new file mode 100644
index 0000000..d65e21d
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit.d/postinst
@@ -0,0 +1,15 @@
+install -m0600 /dev/null /etc/homefs.key
+head -c512 /dev/urandom >/etc/homefs.key
+cryptsetup luksFormat --batch-mode \
+ --key-file=/etc/homefs.key \
+ --type=luks2 \
+ --pbkdf=argon2id \
+ --pbkdf-force-iterations=4 \
+ --pbkdf-memory=32 \
+ -- /dev/vda3
+cryptsetup luksOpen --key-file=/etc/homefs.key --allow-discards \
+ -- /dev/vda3 "vda3_crypt"
+mke2fs -Ft ext4 /dev/mapper/vda3_crypt
+cryptsetup close "vda3_crypt"
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-sysvinit.d/preinst b/debian/tests/cryptroot-sysvinit.d/preinst
new file mode 100644
index 0000000..05157ca
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit.d/preinst
@@ -0,0 +1,16 @@
+cat >/etc/crypttab <<-EOF
+ vda3_crypt /dev/vda3 /etc/homefs.key luks,discard
+ vda4_crypt /dev/vda4 /dev/urandom plain,cipher=aes-xts-plain64,size=256,discard,swap
+ vda5_crypt UUID=$(blkid -s UUID -o value /dev/vda5) none luks,discard
+EOF
+
+cat >/etc/fstab <<-EOF
+ /dev/mapper/vda3_crypt /home auto defaults 0 2
+ /dev/mapper/vda4_crypt none swap sw 0 0
+ /dev/mapper/vda5_crypt / auto errors=remount-ro 0 1
+ UUID=$(blkid -s UUID -o value /dev/vda2) /boot auto defaults 0 2
+EOF
+
+echo "RESUME=none" >/etc/initramfs-tools/conf.d/resume
+
+# vim: set filetype=sh :
diff --git a/debian/tests/cryptroot-sysvinit.d/setup b/debian/tests/cryptroot-sysvinit.d/setup
new file mode 100644
index 0000000..f8598a6
--- /dev/null
+++ b/debian/tests/cryptroot-sysvinit.d/setup
@@ -0,0 +1,43 @@
+# Separate encrypted root FS and /home partitions, and transient swap --
+# the latter two are not unlocked at initramfs stage but later in the
+# boot process. This environment also uses sysvinit as PID1 so we can
+# test our init scripts.
+
+sfdisk --append /dev/vda <<-EOF
+ unit: sectors
+
+ start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS}
+ start=$(((64+128)*1024*2)), size=$((64*1024*2)), type=${GUID_TYPE_LUKS}
+ start=$(((64+128+64)*1024*2)), size=$((64*1024*2)), type=${GUID_TYPE_DMCRYPT}
+ start=$(((64+128+64+64)*1024*2)), type=${GUID_TYPE_LUKS}
+EOF
+udevadm settle
+
+# initialize a new LUKS partition and open it
+echo -n "topsecret" >/rootfs.key
+cryptsetup luksFormat --batch-mode \
+ --key-file=/rootfs.key \
+ --type=luks2 \
+ --pbkdf=argon2id \
+ --pbkdf-force-iterations=4 \
+ --pbkdf-memory=32 \
+ -- /dev/vda5
+cryptsetup luksOpen --key-file=/rootfs.key --allow-discards \
+ -- /dev/vda5 "vda5_crypt"
+udevadm settle
+
+cryptsetup open --type=plain --key-file=/dev/urandom --allow-discards \
+ -- /dev/vda4 "vda4_crypt"
+udevadm settle
+
+mke2fs -Ft ext4 /dev/mapper/vda5_crypt
+mount -t ext4 /dev/mapper/vda5_crypt "$ROOT"
+
+mkdir "$ROOT/boot"
+mke2fs -Ft ext2 -m0 /dev/vda2
+mount -t ext2 /dev/vda2 "$ROOT/boot"
+
+mkswap /dev/mapper/vda4_crypt
+swapon /dev/mapper/vda4_crypt
+
+# vim: set filetype=sh :
diff --git a/debian/tests/initramfs-hook b/debian/tests/initramfs-hook
new file mode 100755
index 0000000..4171102
--- /dev/null
+++ b/debian/tests/initramfs-hook
@@ -0,0 +1,267 @@
+#!/bin/bash
+
+set -eux
+PATH="/usr/bin:/bin:/usr/sbin:/sbin"
+export PATH
+
+TMPDIR="$AUTOPKGTEST_TMP"
+
+# wrappers
+luks1Format() {
+ cryptsetup luksFormat --batch-mode --type=luks1 \
+ --pbkdf-force-iterations=1000 \
+ "$@"
+}
+luks2Format() {
+ cryptsetup luksFormat --batch-mode --type=luks2 \
+ --pbkdf=argon2id --pbkdf-force-iterations=4 --pbkdf-memory=32 \
+ "$@"
+}
+diff() { command diff --color=auto --text "$@"; }
+
+# create disk image
+CRYPT_IMG="$TMPDIR/disk.img"
+CRYPT_DEV=""
+install -m0600 /dev/null "$TMPDIR/keyfile"
+disk_setup() {
+ local lo
+ for lo in $(losetup -j "$CRYPT_IMG" | cut -sd: -f1); do
+ losetup -d "$lo"
+ done
+ dd if="/dev/zero" of="$CRYPT_IMG" bs=1M count=64
+ CRYPT_DEV="$(losetup --find --show -- "$CRYPT_IMG")"
+}
+
+# custom initramfs-tools configuration (to speed things up -- we use
+# COMPRESS=zstd since it's reasonably fast and COMPRESS=none is not
+# supported)
+mkdir "$TMPDIR/initramfs-tools"
+mkdir "$TMPDIR/initramfs-tools/conf.d" \
+ "$TMPDIR/initramfs-tools/scripts" \
+ "$TMPDIR/initramfs-tools/hooks"
+cat >"$TMPDIR/initramfs-tools/initramfs.conf" <<-EOF
+ COMPRESS=zstd
+ MODULES=list
+ RESUME=none
+ UMASK=0077
+EOF
+
+INITRD_IMG="$TMPDIR/initrd.img"
+INITRD_DIR="$TMPDIR/initrd"
+cleanup_initrd_dir() {
+ local d
+ for d in dev proc sys; do
+ mountpoint -q "$INITRD_DIR/$d" && umount "$INITRD_DIR/$d" || true
+ done
+ rm -rf --one-file-system -- "$INITRD_DIR"
+}
+trap cleanup_initrd_dir EXIT INT TERM
+
+mkinitramfs() {
+ local d
+ command mkinitramfs -d "$TMPDIR/initramfs-tools" -o "$INITRD_IMG"
+ # `mkinitramfs -k` would be better but we can't set $DESTDIR in advance
+ cleanup_initrd_dir
+ command unmkinitramfs "$INITRD_IMG" "$INITRD_DIR"
+ for d in dev proc sys; do
+ mkdir -p "$INITRD_DIR/$d"
+ mount --bind "/$d" "$INITRD_DIR/$d"
+ done
+}
+check_initrd_crypttab() {
+ local rv=0 err="${1+": $1"}"
+ diff --label=a/cryptroot/crypttab --label=b/cryptroot/crypttab \
+ --unified --ignore-space-change \
+ -- - "$INITRD_DIR/cryptroot/crypttab" || rv=$?
+ if [ $rv -ne 0 ]; then
+ printf "ERROR$err in file %s line %d\\n" "${BASH_SOURCE[0]}" ${BASH_LINENO[0]} >&2
+ exit 1
+ fi
+}
+
+
+#######################################################################
+# make sure /cryptroot/crypttab is empty when nothing needs to be unclocked early
+
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen "$CRYPT_DEV" test0_crypt <"$TMPDIR/passphrase"
+cat >/etc/crypttab <<-EOF
+ test0_crypt $CRYPT_DEV none
+EOF
+
+mkinitramfs
+# make sure cryptsetup exists and doesn't crash (for instance due to missing libraries) in initrd
+chroot "$INITRD_DIR" cryptsetup --version
+test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1
+check_initrd_crypttab </dev/null
+
+
+#######################################################################
+# 'initramfs' crypttab option
+
+cat >/etc/crypttab <<-EOF
+ test0_crypt $CRYPT_DEV none initramfs
+EOF
+
+mkinitramfs
+chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup close test0_crypt
+check_initrd_crypttab <<-EOF
+ test0_crypt UUID=$(blkid -s UUID -o value "$CRYPT_DEV") none initramfs
+EOF
+
+
+#######################################################################
+# KEYFILE_PATTERN
+
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen "$CRYPT_DEV" test1_crypt <"$TMPDIR/passphrase"
+cat >/etc/crypttab <<-EOF
+ test1_crypt $CRYPT_DEV $TMPDIR/keyfile initramfs
+EOF
+
+echo KEYFILE_PATTERN="$TMPDIR/keyfile" >>/etc/cryptsetup-initramfs/conf-hook
+tr -d '\n' <"$TMPDIR/passphrase" >"$TMPDIR/keyfile"
+mkinitramfs
+check_initrd_crypttab <<-EOF
+ test1_crypt UUID=$(blkid -s UUID -o value "$CRYPT_DEV") /cryptroot/keyfiles/test1_crypt.key initramfs
+EOF
+test -f "$INITRD_DIR/cryptroot/keyfiles/test1_crypt.key" || exit 1
+chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase --key-file="/cryptroot/keyfiles/test1_crypt.key" "$CRYPT_DEV"
+cryptsetup close test1_crypt
+
+
+#######################################################################
+# ASKPASS
+
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen "$CRYPT_DEV" test2_crypt <"$TMPDIR/passphrase"
+cat >/etc/crypttab <<-EOF
+ test2_crypt $CRYPT_DEV none initramfs
+EOF
+
+# interactive unlocking forces ASKPASS=y
+echo ASKPASS=n >/etc/cryptsetup-initramfs/conf-hook
+mkinitramfs
+test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1
+
+# check that unlocking via keyscript doesn't copy askpass
+cat >/etc/crypttab <<-EOF
+ test2_crypt $CRYPT_DEV foobar initramfs,keyscript=passdev
+EOF
+mkinitramfs
+! test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1
+test -f "$INITRD_DIR/lib/cryptsetup/scripts/passdev" || exit 1
+
+# check that unlocking via keyfile doesn't copy askpass
+echo KEYFILE_PATTERN="$TMPDIR/keyfile" >>/etc/cryptsetup-initramfs/conf-hook
+tr -d '\n' <"$TMPDIR/passphrase" >"$TMPDIR/keyfile"
+cat >/etc/crypttab <<-EOF
+ test2_crypt $CRYPT_DEV $TMPDIR/keyfile initramfs
+EOF
+mkinitramfs
+! test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1
+chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase --key-file="/cryptroot/keyfiles/test2_crypt.key" "$CRYPT_DEV"
+cryptsetup close test2_crypt
+
+
+#######################################################################
+# legacy ciphers and hashes
+# see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/merge_requests/31
+
+# LUKS2, blowfish
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format --cipher="blowfish" -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+echo "test3_crypt UUID=$(blkid -s UUID -o value "$CRYPT_DEV") none initramfs" >/etc/crypttab
+mkinitramfs
+legacy_so="$(find "$INITRD_DIR" -xdev -type f -path "*/ossl-modules/legacy.so")"
+test -z "$legacy_so" || exit 1 # legacy ciphers don't need legacy.so
+chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup close test3_crypt
+
+# plain, blowfish + ripemd160 (ignored due to keyfile)
+disk_setup
+head -c32 /dev/urandom >"$TMPDIR/keyfile"
+cryptsetup open --type=plain --cipher="blowfish" --key-file="$TMPDIR/keyfile" --size=256 --hash="ripemd160" "$CRYPT_DEV" test3_crypt
+mkfs.ext2 -m0 /dev/mapper/test3_crypt
+echo "test3_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=blowfish,hash=ripemd160,size=256,initramfs" >/etc/crypttab
+mkinitramfs
+legacy_so="$(find "$INITRD_DIR" -xdev -type f -path "*/ossl-modules/legacy.so")"
+test -z "$legacy_so" || exit 1 # don't need legacy.so here
+volume_key="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)"
+test -n "$volume_key" || exit 1
+cryptsetup close test3_crypt
+chroot "$INITRD_DIR" /scripts/local-top/cryptroot
+test -b /dev/mapper/test3_crypt || exit 1
+volume_key2="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)"
+test "$volume_key" = "$volume_key2" || exit 1
+cryptsetup close test3_crypt
+
+# plain, ripemd160
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+cryptsetup open --type=plain --cipher="aes-cbc-essiv:sha256" --size=256 --hash="ripemd160" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+echo "test3_crypt $CRYPT_DEV none plain,cipher=aes-cbc-essiv:sha256,hash=ripemd160,size=256,initramfs" >/etc/crypttab
+mkinitramfs
+legacy_so="$(find "$INITRD_DIR" -xdev -type f -path "*/ossl-modules/legacy.so")"
+test -n "$legacy_so" || exit 1 # checks that we have legacy.so (positive check for the above)
+volume_key="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)"
+test -n "$volume_key" || exit 1
+cryptsetup close test3_crypt
+chroot "$INITRD_DIR" cryptsetup open --type=plain --cipher="aes-cbc-essiv:sha256" --size=256 --hash="ripemd160" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+test -b /dev/mapper/test3_crypt || exit 1
+volume_key2="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)"
+test "$volume_key" = "$volume_key2" || exit 1
+cryptsetup close test3_crypt
+
+# LUKS1, whirlpool
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks1Format --hash="whirlpool" -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+echo "test3_crypt $CRYPT_DEV none initramfs" >/etc/crypttab
+mkinitramfs
+chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup close test3_crypt
+
+# LUKS2, ripemd160
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format --hash="ripemd160" -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+echo "test3_crypt $CRYPT_DEV none initramfs" >/etc/crypttab
+mkinitramfs
+chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup close test3_crypt
+
+# LUKS2 (detached header), ripemd160
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format --hash="ripemd160" --header="$TMPDIR/header.img" -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen --header="$TMPDIR/header.img" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+echo "test3_crypt $CRYPT_DEV none header=$TMPDIR/header.img,initramfs" >/etc/crypttab
+mkinitramfs
+cp -T "$TMPDIR/header.img" "$INITRD_DIR/cryptroot/header.img"
+chroot "$INITRD_DIR" cryptsetup luksOpen --header="/cryptroot/header.img" --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup close test3_crypt
+rm -f "$TMPDIR/header.img"
+
+# LUKS2 (detached header, missing), ripemd160
+disk_setup
+cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase"
+luks2Format --hash="ripemd160" --header="$TMPDIR/header.img" -- "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup luksOpen --header="$TMPDIR/header.img" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase"
+echo "test3_crypt $CRYPT_DEV none header=/nonexistent,initramfs" >/etc/crypttab
+mkinitramfs
+cp -T "$TMPDIR/header.img" "$INITRD_DIR/cryptroot/header.img"
+chroot "$INITRD_DIR" cryptsetup luksOpen --header="/cryptroot/header.img" --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase"
+cryptsetup close test3_crypt
+rm -f "$TMPDIR/header.img"
diff --git a/debian/tests/utils/cryptroot-common b/debian/tests/utils/cryptroot-common
new file mode 100755
index 0000000..a7df37f
--- /dev/null
+++ b/debian/tests/utils/cryptroot-common
@@ -0,0 +1,537 @@
+#!/bin/bash
+
+# Base test file for cryptroot testing in KVM guests
+#
+# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -eu
+
+TESTNAME="$(basename -- "$0")"
+TESTDIR="$(dirname -- "$0")"
+INTERACTIVE="n" # set to "y" to interact with the guest instead of mocking the session
+export TESTNAME TESTDIR
+
+declare -a EXTRA_REPOS=( "$@" ) # blindly append any extra arguments to sources.list
+START_TIME="$(printf "%(%s)T")"
+
+# Try to create /dev/kvm if missing, for instance in a chroot where /dev isn't managed by udev.
+# Then we can drop root privileges and run the rest of the script as a normal user
+if uid="$(id -u)" && [ $uid -eq 0 ]; then
+ if [ ! -c /dev/kvm ] && mknod -m0600 /dev/kvm c 10 232; then
+ echo "INFO: Created character special file /dev/kvm" >&2
+ fi
+ if [ -z "${AUTOPKGTEST_NORMAL_USER-}" ]; then
+ echo "WARN: \$AUTOPKGTEST_NORMAL_USER is empty or unset, preserving root privileges!" >&2
+ else
+ chown --from="root" -- "$AUTOPKGTEST_NORMAL_USER:" "$AUTOPKGTEST_TMP"
+ if [ -c /dev/kvm ]; then
+ if getent group kvm >/dev/null && chgrp -c kvm /dev/kvm; then
+ # kvm group is created by udev.postinst
+ chmod -c 0660 /dev/kvm
+ usermod -a -G kvm -- "$AUTOPKGTEST_NORMAL_USER"
+ else
+ chown -c -- "$AUTOPKGTEST_NORMAL_USER" "/dev/kvm"
+ fi
+ fi
+ echo "INFO: Dropping root privileges: re-executing as user '$AUTOPKGTEST_NORMAL_USER'" >&2
+ exec runuser -u "$AUTOPKGTEST_NORMAL_USER" -- "$0" "$@"
+ exit 1
+ fi
+fi
+
+set -x
+PATH="/usr/bin:/bin"
+export PATH
+
+if [ -t 1 ]; then
+ # set VT100 autowrap mode (DECAWM)
+ printf '\033[?7h'
+fi
+
+# get src:cryptsetup current version and distribution
+DEB_VERSION="$(dpkg-parsechangelog -SVERSION)"
+DEB_DISTRIBUTION="$(dpkg-parsechangelog -SDistribution)"
+DEB_BUILD_ARCHITECTURE="$(dpkg-architecture -qDEB_BUILD_ARCH)"
+DEB_BUILD_ARCH_BITS="$(dpkg-architecture -qDEB_BUILD_ARCH_BITS)"
+if [ "$DEB_DISTRIBUTION" = "UNRELEASED" ]; then
+ # take Distribution from the previous entry instead
+ DEB_DISTRIBUTION="$(dpkg-parsechangelog -o1 -c1 -SDistribution)" || DEB_DISTRIBUTION="unstable"
+ echo "WARN: Using Distribution: $DEB_DISTRIBUTION instead of UNRELEASED" >&2
+fi
+
+# determine suitable values for the APT repository Origin (for
+# autopkgtests) and URI (used outside autopkgtests) fields
+load_os_release() {
+ local os_release # see os-release(5)
+ [ -e "/etc/os-release" ] && os_release="/etc/os-release" || os_release="/usr/lib/os-release"
+ . "$os_release"
+}
+case "${DISTRIBUTOR_ID:="$(load_os_release && printf "%s" "${ID,,[A-Z]}")"}" in
+ debian) APT_REPO_ORIGIN="Debian"; APT_REPO_URI="http://deb.debian.org/debian";;
+ # suitable values for derivative can be added here
+ *) echo "ERROR: Unknown distributor ID '$DISTRIBUTOR_ID', can't extract APT origin" >&2;
+ exit 1;;
+esac
+
+# QEMU command and default options
+unset QEMU_MACHINE_TYPE QEMU_ACCEL QEMU_CPU_MODEL QEMU_SMP QEMU_MEMORY BOOT
+if [ -c /dev/kvm ] && dd if=/dev/kvm count=0 status=none; then
+ QEMU_ACCEL="kvm"
+else
+ echo "WARN: KVM is not available, guests will be slow!" >&2
+fi
+case "$DEB_BUILD_ARCHITECTURE" in
+ # see `kvm -machine help` and `kvm -cpu help`
+ amd64|i386)
+ BOOT="bios"
+ if [ "$DEB_BUILD_ARCHITECTURE" = "amd64" ]; then
+ QEMU_SYSTEM_CMD="qemu-system-x86_64"
+ else
+ QEMU_SYSTEM_CMD="qemu-system-$DEB_BUILD_ARCHITECTURE"
+ fi
+ QEMU_MACHINE_TYPE="q35"
+ if [ "${QEMU_ACCEL-}" = "kvm" ]; then
+ QEMU_CPU_MODEL="kvm$DEB_BUILD_ARCH_BITS,+aes,+sha-ni"
+ else
+ QEMU_CPU_MODEL="qemu$DEB_BUILD_ARCH_BITS,-svm,-vmx"
+ fi
+ ;;
+ arm64)
+ BOOT="efi"
+ QEMU_SYSTEM_CMD="qemu-system-aarch64"
+ QEMU_MACHINE_TYPE="virt"
+ QEMU_CPU_MODEL="cortex-a72"
+ ;;
+ armhf)
+ BOOT="efi"
+ QEMU_SYSTEM_CMD="qemu-system-arm"
+ QEMU_MACHINE_TYPE="virt"
+ QEMU_CPU_MODEL="cortex-a15"
+ ;;
+ *) echo "ERROR: Unknown architecture $DEB_BUILD_ARCHITECTURE" >&2; exit 1;;
+esac
+
+if ! command -v "$QEMU_SYSTEM_CMD" >/dev/null; then
+ echo "ERROR: Couldn't find $QEMU_SYSTEM_CMD in PATH" >&2
+ exit 1
+fi
+
+CPU_COUNT="$(getconf _NPROCESSORS_ONLN)" && [ -n "$CPU_COUNT" ] || CPU_COUNT=0
+if [ $CPU_COUNT -ge 8 ]; then
+ QEMU_SMP="cpus=4"
+elif [ $CPU_COUNT -ge 4 ]; then
+ QEMU_SMP="cpus=2"
+else
+ QEMU_SMP="cpus=1"
+fi
+
+MEM_AVAIL="$(awk '/MemAvailable/ { printf "%.0f \n", $2/1024^2 }' </proc/meminfo)" && [ -n "$MEM_AVAIL" ] || MEM_AVAIL=0
+if [ $MEM_AVAIL -gt 2 ] && [ $DEB_BUILD_ARCH_BITS -gt 32 ]; then
+ QEMU_MEMORY="size=2G"
+else
+ QEMU_MEMORY="size=1G"
+fi
+
+# number of times to powercycle the guest
+GUEST_POWERCYCLE=0
+
+# kernel flavor
+case "$DEB_BUILD_ARCHITECTURE" in
+ # see `ssh $porterbox.debian.org uname -r`
+ amd64) KERNEL_ARCH="amd64";;
+ arm64) KERNEL_ARCH="arm64";;
+ armhf) KERNEL_ARCH="armmp-lpae";;
+ i386) KERNEL_ARCH="686-pae";;
+ *) echo "ERROR: Unknown architecture $DEB_BUILD_ARCHITECTURE" >&2; exit 1;;
+esac
+
+# at the very least we need a boot loader, a kernel, and an init system
+case "$BOOT" in
+ bios) PKG_BOOTLOADER="grub-pc";;
+ efi) PKG_BOOTLOADER="grub-efi";;
+ *) echo "ERROR unknown boot method '$BOOT'" >&2; exit 1;;
+esac
+PKG_KERNEL="linux-image-$KERNEL_ARCH"
+PKG_INIT="systemd-sysv" # default pid1
+MERGED_USR="" # use default layout for the target version
+declare -a PKGS_EXTRA=() DRIVE_SIZES=( "2G" )
+PKGS_EXTRA+=( "zstd" ) # default initrd compression, see #976054
+
+if [ -f "$TESTDIR/$TESTNAME.d/config" ]; then
+ . "$TESTDIR/$TESTNAME.d/config" || exit 1
+fi
+
+if [ -n "${AUTOPKGTEST_TMP+x}" ] || [ ! -t 0 ] || [ ! -t 1 ]; then
+ INTERACTIVE="n"
+fi
+
+unset EFI_CODE EFI_VARS
+if [ "$BOOT" = "efi" ]; then
+ case "$DEB_BUILD_ARCHITECTURE" in
+ amd64|i386)
+ efi_fw_pkg="ovmf"
+ EFI_CODE="/usr/share/OVMF/OVMF_CODE.fd"
+ EFI_VARS="/usr/share/OVMF/OVMF_VARS.fd"
+ ;;
+ arm64)
+ efi_fw_pkg="qemu-efi-aarch64"
+ EFI_CODE="/usr/share/AAVMF/AAVMF_CODE.fd"
+ EFI_VARS="/usr/share/AAVMF/AAVMF_VARS.fd"
+ ;;
+ armhf)
+ efi_fw_pkg="qemu-efi-arm"
+ EFI_CODE="/usr/share/AAVMF/AAVMF32_CODE.fd"
+ EFI_VARS="/usr/share/AAVMF/AAVMF32_VARS.fd"
+ ;;
+ *) echo "ERROR: Unknown architecture $DEB_BUILD_ARCHITECTURE for EFI boot" >&2; exit 1;;
+ esac
+ for p in "$EFI_CODE" "$EFI_VARS"; do
+ if [ ! -f "$p" ]; then
+ echo "Couldn't find $p, is the '$efi_fw_pkg' package installed?" >&2
+ exit 1
+ fi
+ done
+fi
+
+case "${DEB_DISTRIBUTION%%-*}" in
+ etch|lenny|squeeze|wheezy|jessie|stretch|buster|bullseye)
+ if [ -z "$MERGED_USR" ]; then
+ MERGED_USR="no"
+ fi
+ ;;
+ *) if [ -z "$MERGED_USR" ]; then
+ MERGED_USR="yes"
+ elif [ "$MERGED_USR" = "no" ]; then
+ # #978636: Debian 12 (codename Bookworm) should only support merged-/usr layout
+ echo "WARN: this system is not supported! (unmerged-/usr)" >&2
+ fi
+ ;;
+esac
+
+# pin versions for all packages in PKGS_EXTRA that are part of this source package
+declare -a MYPKGS
+MYPKGS=( $(sed -nr 's/^Package:\s*//Ip' debian/control) )
+for i in "${!PKGS_EXTRA[@]}"; do
+ [ "${PKGS_EXTRA[i]%[=/]*}" = "${PKGS_EXTRA[i]}" ] || continue
+ for mypkg in "${MYPKGS[@]}"; do
+ if [ "${PKGS_EXTRA[i]}" = "$mypkg" ]; then
+ PKGS_EXTRA[i]="${PKGS_EXTRA[i]}=$DEB_VERSION"
+ fi
+ done
+done
+
+unset QEMU_PID
+TEMPDIR="$(mktemp --tmpdir="${AUTOPKGTEST_TMP:-"${TMPDIR:-/tmp}"}" --directory "$TESTNAME.XXXXXXXXXX")"
+teardown() {
+ local rv=$? ts
+ if [ -n "${QEMU_PID+x}" ]; then
+ kill $QEMU_PID || true
+ fi
+ rm -rf -- "$TEMPDIR"
+ trap - EXIT
+
+ # try to fix terminal
+ [ ! -t 1 ] || printf '\033[?7h'
+
+ ts="$(printf "%(%s)T")"
+ rv=${1-$rv}
+ printf "Result for test '%s': exit status %s, runtime %d seconds\\n" "$TESTNAME" $rv $((ts - START_TIME))
+
+ exit $rv
+}
+trap "teardown" EXIT
+trap "teardown 1" INT TERM
+
+# set up APT for the testbed
+setup_apt() {
+ # we need a new cache to reliably determine essential and extra packages
+ APT_CACHE="$TEMPDIR/apt/cache"
+ APT_LISTS="$TEMPDIR/apt/lists"
+ mkdir -- "$TEMPDIR/apt" "$APT_CACHE" "$APT_LISTS"
+ ln -s "cache/archives" "$TEMPDIR/apt/pool"
+ touch "$TEMPDIR/apt/status"
+
+ if [ -n "${AUTOPKGTEST_TMP-}" ]; then
+ # reuse existing sources.list
+ apt-get indextargets \
+ --format "\$(TARGET_OF) \$(REPO_URI) \$(RELEASE) \$(COMPONENT)" \
+ "Target-Of: deb" "Identifier: Packages" "Origin: $APT_REPO_ORIGIN" \
+ >"$TEMPDIR/apt/sources.list"
+ # local autopkgtest repo has Repo-URI: file:/tmp/autopkgtest.XXXXXX/binaries/ ,
+ # Release: (empty) and no Component:
+ apt-get indextargets \
+ --format "\$(TARGET_OF) \$(REPO_URI) /" \
+ "Target-Of: deb" "Identifier: Packages" "Trusted: Yes" "Release: " \
+ >>"$TEMPDIR/apt/sources.list"
+ else
+ # generate new sources.list
+ case "$DEB_DISTRIBUTION" in
+ experimental) cat <<-EOF
+ deb $APT_REPO_URI unstable main
+ deb $APT_REPO_URI experimental main
+ EOF
+ ;;
+ *-security) cat <<-EOF
+ deb $APT_REPO_URI ${DEB_DISTRIBUTION%-security} main
+ deb $APT_REPO_URI-security $DEB_DISTRIBUTION main
+ EOF
+ ;;
+ *-*) cat <<-EOF
+ deb $APT_REPO_URI ${DEB_DISTRIBUTION%%-*} main
+ deb $APT_REPO_URI $DEB_DISTRIBUTION main
+ EOF
+ ;;
+ *) cat <<-EOF
+ deb $APT_REPO_URI $DEB_DISTRIBUTION main
+ EOF
+ ;;
+ esac >"$TEMPDIR/apt/sources.list"
+ fi
+
+ local apt_repo
+ for apt_repo in "${EXTRA_REPOS[@]}"; do
+ printf "%s\\n" "$apt_repo" >>"$TEMPDIR/apt/sources.list"
+ done
+
+ # replace file: URIs with copy: as we rely on --download-only copying .deb files to APT's cache
+ sed -ri 's/^(deb\S*)\s+\[([^]]+)\]\s+file:/\1 [\2,trusted=yes] copy:/;
+ s/^(deb\S*)\s+file:/\1 [trusted=yes] copy:/' \
+ -- "$TEMPDIR/apt/sources.list"
+
+ apt-update
+}
+
+# wrapper arround `apt-get install --download-only`
+# (we don't use `--print-uris` since it doesn't include what's been
+# included already)
+apt-download() {
+ _apt get install --download-only "$@"
+}
+apt-update() {
+ _apt get -o Acquire::Languages="none" update
+}
+apt-show() {
+ _apt cache show "$@"
+}
+_apt() {
+ local cmd="$1"
+ shift
+ env -i DEBIAN_FRONTEND="noninteractive" \
+ "apt-$cmd" \
+ -o APT::Architecture="$DEB_BUILD_ARCHITECTURE" \
+ -o APT::Architectures="$DEB_BUILD_ARCHITECTURE" \
+ -o APT::Get::Assume-Yes=true \
+ -o APT::Install-Recommends=false \
+ -o Dir::Cache="$APT_CACHE" \
+ -o Dir::Etc::SourceList="$TEMPDIR/apt/sources.list" \
+ -o Dir::Etc::SourceParts="" \
+ -o Dir::State::Lists="$APT_LISTS" \
+ -o Dir::State::Status="$TEMPDIR/apt/status" \
+ ${AUTOPKGTEST_TMP+-o Dir::Etc::Preferences="/etc/apt/preferences" -o Dir::Etc::PreferencesParts="/etc/apt/preferences.d/"} \
+ "$@"
+}
+
+
+# create a disk image with essential and extra packages
+create_debian_img() {
+ local img="$1" dir size deb usr_is_merged
+
+ dir="$(mktemp --tmpdir="$TEMPDIR" --directory debian.XXXXXXXXXX)"
+ mkdir -- "$dir/dists" "$dir/pool"
+
+ # TODO remove this once Bookworm is released, assuming
+ # init-system-helpers no longer has "Depends: usrmerge | usr-is-merged"
+ [ "$MERGED_USR" = "yes" ] && usr_is_merged="usr-is-merged" || usr_is_merged=""
+
+ # apt considers itself essential so we explicitely exclude it for stage1
+ mkdir -- "$dir/__stage1__"
+ apt-download -- "?and(?essential, ?not(?exact-name(apt)))" ${usr_is_merged:+"$usr_is_merged"}
+ for deb in "$APT_CACHE"/archives/*.deb; do
+ ln -sT "../pool/${deb##*/}" "$dir/__stage1__/${deb##*/}"
+ done
+
+ # useless for stage1
+ rm -f "$dir"/__stage1__/usr-is-merged_*.deb "$dir"/__stage1__/usrmerge_*.deb
+
+ mkdir -- "$dir/__essential__"
+ apt-download -- "?essential" "apt" ${usr_is_merged:+"$usr_is_merged"}
+ for deb in "$APT_CACHE"/archives/*.deb; do
+ ln -sT "../pool/${deb##*/}" "$dir/__essential__/${deb##*/}"
+ done
+
+ makedist "$dir"
+ extract_kernel "$TEMPDIR/linux-image"
+
+ # for `dpkg --update-avail`
+ ( cd "$dir/__essential__" && dpkg-scanpackages . >./Packages )
+
+ size="$(du -sb -- "$dir")"
+ size=$(( ${size%%[!0-9]*} / 1000 )) # approx 97% (1000/1024) full
+ genext2fs -qm0 -B 1024 -b "$size" -d "$dir" -L "debian_dist" "$img"
+ rm -rf -- "$dir"
+}
+makedist() {
+ local basedir="$1"
+ local distdir="$basedir/dists"
+ apt-download -- "?essential" "apt" ${usr_is_merged:+"$usr_is_merged"} \
+ "$PKG_BOOTLOADER" "$PKG_KERNEL" "$PKG_INIT" \
+ "${PKGS_EXTRA[@]}"
+ rm -f -- "$APT_CACHE/archives/$PKG_KERNEL"_*.deb # remove the generic .deb (only keep its dependency with versioned ABI)
+ for deb in "$APT_CACHE"/archives/*.deb; do
+ # assume no file conflicts and override existing .debs
+ ln -ft "$basedir/pool" -- "$deb"
+ done
+ ( cd "$APT_CACHE" && dpkg-scanpackages ../pool >"$distdir/Packages" )
+}
+
+# extract kernel to $TEMPDIR/linux-image and sets KERNEL_VERSION
+extract_kernel() {
+ local destdir="$1" deb_version_regex kernel_deb_regex
+ deb_version_regex="[0-9][A-Za-z0-9.+:~-]*" # per deb-version(7)
+ # we use may a kernel version other than what we're running, however the arch much be the same
+ kernel_deb_regex="linux-image-[0-9][a-z0-9.+-]*-${KERNEL_ARCH}_${deb_version_regex}_${DEB_BUILD_ARCHITECTURE}.deb"
+ KERNEL_DEB="$(find -P "$APT_CACHE/archives" -mindepth 1 -maxdepth 1 \
+ -regextype egrep -regex ".*/$kernel_deb_regex" -type f -printf "%P\\n" | \
+ sort -Vt_ -k2 | tail -n1)"
+ KERNEL_VERSION="${KERNEL_DEB#linux-image-*}"
+ KERNEL_VERSION="${KERNEL_VERSION%%_*}"
+
+ # extract the kernel of the .deb we downloaded
+ if [ ! -f "$APT_CACHE/archives/$KERNEL_DEB" ]; then
+ echo "ERROR: Couldn't find .deb for target kernel $KERNEL_VERSION" >&2
+ exit 1
+ fi
+
+ mkdir "$destdir"
+ dpkg-deb --fsys-tarfile "$APT_CACHE/archives/$KERNEL_DEB" | tar -C "$destdir" -xf- \
+ "./boot/vmlinuz-$KERNEL_VERSION" \
+ "./lib/modules/$KERNEL_VERSION"
+ ln -T -- "$destdir/boot/vmlinuz-$KERNEL_VERSION" "$TEMPDIR/vmlinuz-$KERNEL_VERSION"
+}
+
+# make sure the desired version of the package is available in the testbed
+setup_apt
+if ! apt-show "cryptsetup-bin=$DEB_VERSION" >"$TEMPDIR/out" || [ ! -s "$TEMPDIR/out" ]; then
+ apt-show -a "cryptsetup-bin" || true
+ echo "ERROR: Cannot find version $DEB_VERSION of package cryptsetup-bin" >&2
+ exit 1
+fi
+
+DEBIAN_IMG="$TEMPDIR/$DEB_DISTRIBUTION-$DEB_BUILD_ARCHITECTURE.img"
+create_debian_img "$DEBIAN_IMG"
+
+case "$DEB_BUILD_ARCHITECTURE" in
+ arm64|armhf) CONSOLE="ttyAMA0";;
+ *) CONSOLE="ttyS0";;
+esac
+
+env PACKAGES="$PKG_BOOTLOADER linux-image-$KERNEL_VERSION $PKG_INIT ${PKGS_EXTRA[*]}" \
+ BOOT="$BOOT" \
+ CONSOLE="$CONSOLE" \
+ ARCH="$DEB_BUILD_ARCHITECTURE" \
+ MERGED_USR="$MERGED_USR" \
+ "$TESTDIR/utils/mkinitramfs" "$TEMPDIR/linux-image" "$KERNEL_VERSION" "$TEMPDIR/initrd.img-$KERNEL_VERSION"
+rm -rf -- "$TEMPDIR/apt" "$TEMPDIR/linux-image" # don't need that anymore
+
+declare -a QEMU_COMMON_ARGS=(
+ -no-user-config
+ -nodefaults
+ -name "autopkgtest-cryptsetup-$TESTNAME"
+ -machine "${QEMU_MACHINE_TYPE:+"type=$QEMU_MACHINE_TYPE,"}${QEMU_ACCEL:+"accel=$QEMU_ACCEL,"}graphics=off"
+ ${QEMU_CPU_MODEL:+-cpu "$QEMU_CPU_MODEL"}
+ ${QEMU_SMP:+-smp "$QEMU_SMP"}
+ ${QEMU_MEMORY:+-m "$QEMU_MEMORY"}
+ -vga none
+ -display none
+ -object "rng-random,id=rng0,filename=/dev/urandom" -device "virtio-rng-pci,rng=rng0"
+ -boot "order=c,strict=on"
+)
+
+for ((i=0; i < ${#DRIVE_SIZES[@]}; i++)); do
+ drive_img="$TEMPDIR/drive$i.img"
+ fallocate -l "${DRIVE_SIZES[i]}" "$drive_img"
+ QEMU_COMMON_ARGS+=(
+ -drive "file=$drive_img,format=raw,cache=unsafe,if=virtio,index=$i,media=disk"
+ )
+done
+
+if [ "$BOOT" = "efi" ]; then
+ # $EFI_VARS needs to be writable so guests can update their variables
+ install -Tm0644 -- "$EFI_VARS" "$TEMPDIR/efivars.fd"
+ QEMU_COMMON_ARGS+=(
+ -drive "file=$EFI_CODE,format=raw,if=pflash,unit=0,read-only=on"
+ -drive "file=$TEMPDIR/efivars.fd,format=raw,if=pflash,unit=1"
+ )
+fi
+
+LOGDIR="$TEMPDIR"
+SOCKETDIR="$TEMPDIR"
+if [ "$INTERACTIVE" != "y" ]; then
+ QEMU_COMMON_ARGS+=(
+ -device "virtio-serial"
+ -chardev "socket,id=hvc0,path=$SOCKETDIR/hvc0,server=on,wait=off,logfile=$LOGDIR/hvc0.log,logappend=on"
+ -device "virtconsole,chardev=hvc0"
+ )
+fi
+
+declare QEMU_STDIO_ARGS=(
+ # setup is always fully unattended
+ -chardev "stdio,id=char0,mux=on,logfile=$LOGDIR/qemu.log,logappend=on"
+ -serial "chardev:char0"
+ -mon "chardev=char0,mode=readline"
+)
+if [ "$INTERACTIVE" != "y" ] || [ -n "${AUTOPKGTEST_TMP+x}" ]; then
+ # XXX if KVM is detected we could reduce the timeout to 300s or so
+ QEMU_TIMEOUT="y"
+ exec </dev/null
+else
+ QEMU_TIMEOUT=""
+fi
+
+QEMU_DEBIANIMG_DRIVE="file=$DEBIAN_IMG,format=raw,if=virtio,readonly=on,media=cdrom"
+${QEMU_TIMEOUT:+timeout 3600s} "$QEMU_SYSTEM_CMD" \
+ "${QEMU_COMMON_ARGS[@]}" "${QEMU_STDIO_ARGS[@]}" \
+ -drive "$QEMU_DEBIANIMG_DRIVE" \
+ -kernel "$TEMPDIR/vmlinuz-$KERNEL_VERSION" \
+ -append "console=$CONSOLE,115200n8" \
+ -initrd "$TEMPDIR/initrd.img-$KERNEL_VERSION" \
+ || exit $?
+
+if [ "$INTERACTIVE" = "y" ]; then
+ for ((i=0; i <= GUEST_POWERCYCLE; i++)); do
+ "$QEMU_SYSTEM_CMD" \
+ "${QEMU_COMMON_ARGS[@]}" "${QEMU_STDIO_ARGS[@]}" \
+ -netdev "user,id=net0" -device "virtio-net-pci,netdev=net0"
+ done
+else
+ for ((i=0; i <= GUEST_POWERCYCLE; i++)); do
+ ${QEMU_TIMEOUT:+timeout 900s} "$QEMU_SYSTEM_CMD" \
+ "${QEMU_COMMON_ARGS[@]}" \
+ -chardev "socket,id=mon0,path=$SOCKETDIR/mon0,server=on,wait=off,logfile=$LOGDIR/mon0.log,logappend=on" \
+ -mon "chardev=mon0,mode=control" \
+ -chardev "socket,id=ttyS0,path=$SOCKETDIR/ttyS0,server=on,wait=on,logfile=$LOGDIR/ttyS0.log,logappend=on" \
+ -serial "chardev:ttyS0" \
+ &
+ QEMU_PID=$!
+ "$TESTDIR/$TESTNAME.d/mock" "$i" "$SOCKETDIR" || exit 1
+ wait $QEMU_PID && rv=0 || rv=$?
+ unset QEMU_PID
+ [ $rv -eq 0 ] || exit $rv
+ done
+fi
+
+echo "PASSED"
+exit 0
diff --git a/debian/tests/utils/debootstrap b/debian/tests/utils/debootstrap
new file mode 100755
index 0000000..258be5a
--- /dev/null
+++ b/debian/tests/utils/debootstrap
@@ -0,0 +1,125 @@
+#!/bin/sh
+
+# Debootstrap a target system
+#
+# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -eu
+PATH="/usr/sbin:/usr/bin:/sbin:/bin"
+export PATH
+
+ESSENTIAL="/media/__essential__"
+TEMPDIR="$(mktemp --tmpdir --directory "debootstrap.XXXXXXXXXX")"
+trap "rm -rf -- \"$TEMPDIR\"" EXIT INT TERM
+
+sed -rn "/^Package:\\s*/I {s///;s/$/ install/p}" "$ESSENTIAL/Packages" >"$TEMPDIR/Packages.sel"
+
+install -m0644 /dev/null "/var/lib/dpkg/status"
+dpkg --update-avail "$ESSENTIAL/Packages"
+dpkg --set-selections <"$TEMPDIR/Packages.sel"
+
+mkdir -- "$TEMPDIR/dpkg"
+mkdir -- "$TEMPDIR/dpkg/files" "$TEMPDIR/dpkg/depends" "$TEMPDIR/dpkg/pre-depends"
+
+# extract metadata (package names, file names, Depends and Pre-Depends
+# for easier processing)
+for deb in "$ESSENTIAL"/*.deb; do
+ pkg=$(dpkg-deb --show --showformat="\${Package}" "$deb")
+ case "$pkg" in
+ # special case: base-files Pre-Depends on awk but we only have mawk (or gawk)
+ mawk|gawk) pkg="awk";;
+ esac
+ printf "%s\\n" "$pkg" >>"$TEMPDIR/dpkg/avail"
+ printf "%s\\n" "$deb" >"$TEMPDIR/dpkg/files/$pkg"
+ dpkg-deb --show --showformat="\${Pre-Depends}\\n" "$deb" >"$TEMPDIR/predeps"
+ dpkg-deb --show --showformat="\${Depends}\\n" "$deb" >"$TEMPDIR/deps"
+ sed -ri "s/,\\s*/\\n/g" -- "$TEMPDIR/predeps" "$TEMPDIR/deps"
+ sed -i "s/[[:blank:]:].*//; /^[[:blank:]]*$/d" -- "$TEMPDIR/predeps" "$TEMPDIR/deps"
+ mv -T -- "$TEMPDIR/predeps" "$TEMPDIR/dpkg/pre-depends/$pkg"
+ mv -T -- "$TEMPDIR/deps" "$TEMPDIR/dpkg/depends/$pkg"
+done
+
+if [ -L /bin ] && [ -L /sbin ] && [ -L /lib ]; then
+ # TODO remove this once Bookworm is released, assuming
+ # init-system-helpers no longer has "Depends: usrmerge | usr-is-merged"
+ sed -i "s/^usrmerge$/usr-is-merged/" -- "$TEMPDIR/dpkg/depends/init-system-helpers"
+fi
+
+# recursively append dependencies to $OUT; abort and return 1 if one of
+# the (recursive) dependency has an unsatisfied Pre-Depends
+resolve_deps() {
+ local pkg="$1" dep
+ while read -r dep; do
+ if grep -Fxq -e "$dep" <"$TEMPDIR/dpkg/avail"; then
+ # $pkg has an unsatisfied Pre-Depends, can't proceed further
+ return 1
+ fi
+ done <"$TEMPDIR/dpkg/pre-depends/$pkg"
+ while read -r dep; do
+ if grep -Fxq -e "$dep" <"$TEMPDIR/dpkg/avail" && ! grep -Fxq -e "$dep" <"$OUT"; then # break cycles
+ printf "%s\\n" "$dep" >>"$OUT"
+ resolve_deps "$dep" || return $?
+ fi
+ done <"$TEMPDIR/dpkg/depends/$pkg"
+ return 0
+}
+
+# dump to $OUT a list of packages that can be installed (only packages
+# without unsatisfied pre-dependencies, and typically packages that are
+# pre-dependencies of other packages) -- using `dpkg --predep-package`
+# would be convenient but it doesn't work with recursive dependencies,
+# cf. #539133
+can_install_next() {
+ local pkg
+ while read -r pkg; do
+ printf "%s\\n" "$pkg" >"$OUT"
+ if resolve_deps "$pkg"; then
+ return 0
+ fi
+ done <"$TEMPDIR/dpkg/avail"
+
+ echo "PANIC: No remaining dependencies are satisfiable!" >&2
+ cat <"$TEMPDIR/dpkg/avail" >&2
+ exit 1
+}
+
+# keep going until all available packages are installed
+OUT="$TEMPDIR/pkg.list"
+XARGS_IN="$TEMPDIR/deb.list"
+while [ -s "$TEMPDIR/dpkg/avail" ]; do
+ can_install_next || exit 1
+
+ echo -n ">>> Installing: " >&2
+ paste -sd" " <"$OUT" >&2
+
+ while read -r pkg; do
+ cat "$TEMPDIR/dpkg/files/$pkg"
+ done <"$OUT" >"$XARGS_IN"
+ xargs -a"$XARGS_IN" -d"\\n" dpkg -i
+
+ grep -Fx -vf "$OUT" <"$TEMPDIR/dpkg/avail" >"$TEMPDIR/dpkg/avail.new" || true
+ mv -T -- "$TEMPDIR/dpkg/avail.new" "$TEMPDIR/dpkg/avail"
+done
+
+echo apt apt >/var/lib/dpkg/cmethopt
+echo "deb [trusted=yes] file:/media/dists /" >/etc/apt/sources.list
+cat >/etc/apt/apt.conf.d/99debootstrap <<-EOF
+ Acquire::Languages "none";
+ APT::Install-Recommends "false";
+ APT::Install-Suggests "false";
+EOF
+
+apt-get -oAcquire::Languages="none" -oAPT::Sandbox::User="root" -qq update
diff --git a/debian/tests/utils/init b/debian/tests/utils/init
new file mode 100755
index 0000000..331cd6f
--- /dev/null
+++ b/debian/tests/utils/init
@@ -0,0 +1,273 @@
+#!/bin/sh
+
+# PID1 at initramfs stage
+#
+# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -eux
+PATH="/usr/sbin:/usr/bin:/sbin:/bin"
+export PATH
+
+trap "echo \"ALERT! Couldn't setup system, dropping to a shell.\" >&2; sh -i" 0
+
+# set VT100 autowrap mode again (QEMU might mess the terminal up)
+printf '\033[?7h'
+
+mount -t devtmpfs -o noexec,nosuid,mode=0755 udev /dev
+
+mkdir /dev/pts /proc /run /sys
+mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts
+mount -t proc -o nodev,noexec,nosuid proc /proc
+mount -t tmpfs -o nodev,noexec,nosuid,size=5%,mode=0755 tmpfs /run
+mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
+
+modprobe virtio_rng # /dev/hwrng (avoid entropy starvation)
+modprobe virtio_pci
+modprobe virtio_blk # /dev/vd[a-z]
+modprobe virtio_console # /dev/hvc[0-7]
+
+# start udevd
+/lib/systemd/systemd-udevd --daemon
+udevadm trigger --type=subsystems --action=add
+udevadm trigger --type=devices --action=add
+udevadm settle
+
+. /init.conf
+
+# https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
+GUID_TYPE_MBR="024DEE41-33E7-11D3-9D69-0008C781F39F" # MBR partition scheme
+GUID_TYPE_EFI="C12A7328-F81F-11D2-BA4B-00A0C93EC93B" # EFI boot partition
+GUID_TYPE_BIOS_boot="21686148-6449-6E6F-744E-656564454649" # BIOS boot partition
+GUID_TYPE_Linux_FS="0FC63DAF-8483-4772-8E79-3D69D8477DE4" # Linux filesystem data
+GUID_TYPE_LUKS="CA7D7CCB-63ED-4C53-861C-1742536059CC" # LUKS partition
+GUID_TYPE_DMCRYPT="7FFEC5C9-2D00-49B7-8941-3EA10A5586B7" # Plain dm-crypt partition
+GUID_TYPE_LVM="E6D6D379-F507-44C2-A23C-238F2A3DF928" # Logical Volume Manager partition
+GUID_TYPE_RAID="A19D880F-05FC-4D3B-A006-743F0F84911E" # RAID partition
+
+if [ "$BOOT" = "bios" ]; then
+ BOOT_PARTITION_SIZE=2
+ BOOT_PARTITION_TYPE="$GUID_TYPE_BIOS_boot"
+elif [ "$BOOT" = "efi" ]; then
+ BOOT_PARTITION_SIZE=63
+ BOOT_PARTITION_TYPE="$GUID_TYPE_EFI"
+else
+ echo "ERROR unknown boot method '$BOOT'" >&2
+ exit 1
+fi
+
+# format the target disk and create a BIOS/EFI partition
+sfdisk /dev/vda <<-EOF
+ label: gpt
+ unit: sectors
+
+ start=$((1024*2)), size=$((BOOT_PARTITION_SIZE*1024*2)), type=$BOOT_PARTITION_TYPE
+EOF
+udevadm settle
+
+ROOT="/target"
+mkdir -m0755 "$ROOT"
+# /init.setup is expected to create the root filesystem of the target
+# system and mount it (alongside other filesystems) on $ROOT
+. /init.setup
+udevadm settle
+
+# inspired by debootstrap's /usr/share/debootstrap/functions
+if [ "$MERGED_USR" = "yes" ]; then
+ case "$ARCH" in
+ amd64) libdir="lib32 lib64 libx32";;
+ i386) libdir="lib64 libx32";;
+ mips|mipsel) libdir="lib32 lib64";;
+ mips64*|mipsn32*) libdir="lib32 lib64 libo32";;
+ loongarch64*) libdir="lib32 lib64";;
+ powerpc) libdir="lib64";;
+ ppc64) libdir="lib32 lib64";;
+ ppc64el) libdir="lib64";;
+ s390x) libdir="lib32";;
+ sparc) libdir="lib64";;
+ sparc64) libdir="lib32 lib64";;
+ x32) libdir="lib32 lib64 libx32";;
+ *) libdir="";;
+ esac
+ for dir in bin sbin lib $libdir; do
+ ln -s "usr/$dir" "$ROOT/$dir"
+ mkdir -p "$ROOT/usr/$dir"
+ done
+fi
+
+mkdir /media
+DEBIAN_DIST="$(blkid -l -t LABEL="debian_dist" -o device)"
+mount -t ext2 -o ro "$DEBIAN_DIST" /media
+for pkg in /media/__stage1__/*.deb; do
+ dpkg-deb --fsys-tarfile "$pkg" | tar -C "$ROOT" -xf - --keep-directory-symlink
+done
+
+# setup hosts(5) and hostname(5)
+echo "$HOSTNAME" >"$ROOT/etc/hostname"
+echo "127.0.0.1 localhost $HOSTNAME" >"$ROOT/etc/hosts"
+
+# EFI
+if [ "$BOOT" = "efi" ]; then
+ modprobe efivarfs
+ mount -t efivarfs efivarfs /sys/firmware/efi/efivars
+
+ mkfs.vfat -F 32 /dev/vda1
+ mkdir "$ROOT/boot/efi"
+ mount -t vfat /dev/vda1 "$ROOT/boot/efi"
+
+ cat >>"$ROOT/etc/fstab" <<-EOF
+ UUID=$(blkid -s UUID -o value /dev/vda1) /boot/efi auto defaults 0 2
+ EOF
+fi
+
+# bind mount pseudo and temporary filesystems to "$ROOT"
+mount -no bind /dev "$ROOT/dev"
+mount -no bind /proc "$ROOT/proc"
+mount -no bind /sys "$ROOT/sys"
+mount -t tmpfs -o nodev,noexec,nosuid,size=5%,mode=0755 tmpfs "$ROOT/run"
+
+# prevent any services from starting during package installation, taken
+# from debootstrap(8)
+cat >"$ROOT/usr/sbin/policy-rc.d" <<-EOF
+ #!/bin/sh
+ exit 101
+EOF
+chmod +x "$ROOT/usr/sbin/policy-rc.d"
+
+mv "$ROOT/sbin/start-stop-daemon" "$ROOT/sbin/start-stop-daemon.REAL"
+cat >"$ROOT/sbin/start-stop-daemon" <<-EOF
+ #!/bin/sh
+ echo
+ echo "Warning: Fake start-stop-daemon called, doing nothing"
+EOF
+chmod +x "$ROOT/usr/sbin/policy-rc.d" "$ROOT/sbin/start-stop-daemon"
+
+DEBIAN_FRONTEND="noninteractive"
+DEBCONF_NONINTERACTIVE_SEEN="true"
+export DEBIAN_FRONTEND DEBCONF_NONINTERACTIVE_SEEN
+
+# debootstrap the target system
+mkdir "$ROOT/media"
+mount -no move /media "$ROOT/media"
+cp -p /debootstrap "$ROOT/debootstrap"
+chroot "$ROOT" /debootstrap
+rm -f "$ROOT/debootstrap"
+
+# use MODULES=dep (if it works with fewer modules then it also works
+# with the default MODULES=most)
+mkdir -p "$ROOT/etc/initramfs-tools/conf.d"
+echo "MODULES=dep" >"$ROOT/etc/initramfs-tools/conf.d/modules"
+
+cp /init.preinst "$ROOT/init.preinst"
+chroot "$ROOT" /bin/sh -eux /init.preinst
+rm -f "$ROOT/init.preinst"
+udevadm settle
+
+# install extra packages
+chroot "$ROOT" apt-get -oAPT::Sandbox::User="root" install --yes $PACKAGES
+rm -f "$ROOT/etc/apt/sources.list"
+
+# configure and install GRUB
+cat >"$ROOT/etc/default/grub" <<-EOF
+ GRUB_DEFAULT=0
+ GRUB_TIMEOUT=0
+ GRUB_CMDLINE_LINUX_DEFAULT=""
+ GRUB_CMDLINE_LINUX="console=$CONSOLE,115200n8"
+ GRUB_DISABLE_RECOVERY=true
+ GRUB_TERMINAL="console serial"
+ GRUB_SERIAL_COMMAND="serial --speed=115200"
+EOF
+chroot "$ROOT" grub-install --no-floppy --modules=part_gpt /dev/vda
+chroot "$ROOT" update-grub
+
+chroot "$ROOT" passwd --delete root # make root account passwordless
+
+# show some system info right after login to ease troubleshooting
+cat >"$ROOT/root/.profile" <<-EOF
+ run_verbose() {
+ printf "\\\`%s\\\` output:\\\\n" "\$*"
+ "\$@"
+ }
+ stty cols 150
+ run_verbose dmsetup table
+ run_verbose lsblk
+ run_verbose df -h
+EOF
+
+cat >"$ROOT/root/.inputrc" <<-EOF
+ # disabled bracketed paste mode
+ set enable-bracketed-paste off
+EOF
+
+if [ -d "$ROOT/etc/systemd/system" ]; then
+ # systemd
+ if [ -c "$ROOT/dev/hvc0" ]; then
+ # serial-getty@ttyS0.service is automatically enabled due to the console= kernel parameter
+ ln -s "/dev/null" "$ROOT/etc/systemd/system/serial-getty@ttyS0.service"
+ ln -s "/lib/systemd/system/serial-getty@.service" \
+ "$ROOT/etc/systemd/system/getty.target.wants/serial-getty@hvc0.service"
+ fi
+
+ # mask all timer units
+ for t in "$ROOT"/lib/systemd/system/*.timer; do
+ test -f "$t" || continue
+ ln -s "/dev/null" "$ROOT/etc/systemd/system/${t##*/}"
+ done
+
+ # mask systemd-firstboot.service
+ ln -s "/dev/null" "/root/etc/systemd/system/systemd-firstboot.service"
+fi
+
+if [ -f "$ROOT/etc/inittab" ]; then
+ # sysvinit
+ if [ -c "$ROOT/dev/hvc0" ]; then
+ echo "h0:2345:respawn:/sbin/agetty -8 -L 115200 hvc0 linux"
+ else
+ echo "S0:23:respawn:/sbin/getty -8 -L 115200 $CONSOLE linux"
+ fi >>"$ROOT/etc/inittab"
+fi
+
+if [ -f /init.postinst ]; then
+ cp /init.postinst "$ROOT/init.postinst"
+ chroot "$ROOT" /bin/sh -eux /init.postinst
+ rm -f "$ROOT/init.postinst"
+fi
+
+# allow service startup again
+mv "$ROOT/sbin/start-stop-daemon.REAL" "$ROOT/sbin/start-stop-daemon"
+rm "$ROOT/usr/sbin/policy-rc.d"
+
+# unmount pseudo filesystems from the target system
+umount "$ROOT/dev"
+umount "$ROOT/proc"
+umount "$ROOT/sys"
+
+if [ "$BOOT" = "efi" ]; then
+ umount "$ROOT/boot/efi"
+fi
+umount "$ROOT/media"
+umount "$ROOT/run"
+
+# /init.bottom is expected to umount $ROOT and its submounts
+ROOT="$ROOT" sh -eux /init.bottom
+
+# stop udevd
+udevadm control --exit
+
+# exiting this script yields "Kernel panic - not syncing: Attempted to
+# kill init!", so give the asyncronous SysRq trigger a chance to power
+# off (sending a racy C-d would still trigger a panic but we don't care)
+echo o >/proc/sysrq-trigger
+exec cat >/dev/null
diff --git a/debian/tests/utils/mkinitramfs b/debian/tests/utils/mkinitramfs
new file mode 100755
index 0000000..6bc70f4
--- /dev/null
+++ b/debian/tests/utils/mkinitramfs
@@ -0,0 +1,159 @@
+#!/bin/sh
+
+# Generate an initramfs image, much like mkinitramfs(8) but simpler
+#
+# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -eu
+PATH="/usr/sbin:/usr/bin:/sbin:/bin"
+export PATH
+
+unset DEBUG
+EXTRACT_DIR="$1"
+KERNEL_VERSION="$2"
+INITRD="$3"
+
+UTILS="$(dirname -- "$0")"
+DESTDIR="$(mktemp --directory -- "$INITRD.XXXXXXXXXX")"
+trap "rm -r${DEBUG:+v}f -- \"$DESTDIR\"" EXIT INT TERM
+
+# from /usr/sbin/mkinitramfs: create usr-merged filesystem layout, to
+# avoid duplicates if the host filesystem is usr-merged
+for d in /bin /lib* /sbin; do
+ [ -d "$d" ] || continue
+ mkdir -p "$DESTDIR/usr$d"
+ ln -sT "usr$d" "$DESTDIR$d"
+done
+
+install -m0755 "$UTILS/init" "$DESTDIR/init"
+install -m0755 "$UTILS/debootstrap" "$DESTDIR/debootstrap"
+cat >"$DESTDIR/init.conf" <<- EOF
+ HOSTNAME="$TESTNAME"
+ export HOSTNAME
+ PACKAGES="$PACKAGES"
+ BOOT="$BOOT"
+ CONSOLE="$CONSOLE"
+ ARCH="$ARCH"
+ MERGED_USR="$MERGED_USR"
+EOF
+
+for p in setup preinst postinst bottom; do
+ # setup: sourced after creating the BIOS or EFI boot partition
+ # preinst: run in chroot after debootstrap, but before installing extra packages
+ # postinst: optionally run in chroot after installing extra packages
+ # bottom: last thing to run before shutdown
+ if [ -f "$TESTDIR/$TESTNAME.d/$p" ]; then
+ install -m0755 "$TESTDIR/$TESTNAME.d/$p" "$DESTDIR/init.$p"
+ fi
+done
+
+MODULES="dm_crypt ext4 btrfs raid0 raid1"
+if [ "$BOOT" = "efi" ]; then
+ MODULES="$MODULES efivarfs nls_ascii nls_cp437 vfat"
+fi
+
+depmod -ab "$EXTRACT_DIR" "$KERNEL_VERSION"
+for kmod in virtio_console virtio_blk virtio_pci virtio_rng \
+ "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION"/kernel/arch/*/crypto/*.ko* \
+ "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION"/kernel/crypto/*.ko* \
+ $MODULES; do
+ kmod="${kmod##*/}"
+ modprobe -aid "$EXTRACT_DIR" -S "$KERNEL_VERSION" --show-depends "${kmod%%.*}"
+done | while read -r insmod kmod _; do
+ [ "$insmod" = "insmod" ] || continue
+ kmod_rel="${kmod#"$EXTRACT_DIR/lib/modules/$KERNEL_VERSION/"}"
+ if [ ! -f "$kmod" ] || [ "${kmod_rel#kernel/}" = "$kmod_rel" ]; then
+ echo "Error: Unexpected modprobe output: $insmod $kmod" >&2
+ exit 1
+ fi
+ mkdir -p "$DESTDIR/lib/modules/$KERNEL_VERSION/${kmod_rel%/*}"
+ ln -f${DEBUG:+v}T -- "$kmod" "$DESTDIR/lib/modules/$KERNEL_VERSION/$kmod_rel"
+done
+
+ln -t "$DESTDIR/lib/modules/$KERNEL_VERSION" -- \
+ "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION/modules.order" \
+ "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION/modules.builtin"
+depmod -wab "$DESTDIR" "$KERNEL_VERSION"
+
+verbose="${DEBUG-}"
+. /usr/share/initramfs-tools/hook-functions # for copy_exec()
+if [ -f "$TESTDIR/$TESTNAME.d/mkinitramfs" ]; then
+ . "$TESTDIR/$TESTNAME.d/mkinitramfs"
+fi
+
+copy_exec /bin/cp
+copy_exec /bin/rm
+copy_exec /bin/chmod
+
+copy_exec /sbin/modprobe
+copy_exec /sbin/blkid
+copy_exec /sbin/sfdisk
+copy_exec /sbin/mkswap
+copy_exec /sbin/swapon
+copy_exec /sbin/swapoff
+copy_exec /sbin/cryptsetup
+copy_exec /sbin/dmsetup
+copy_exec /usr/bin/dpkg-deb
+copy_exec /bin/tar
+
+# assume ossl-modules/legacy.so and libgcc_s.so are relative to the linked libcryptsetup.so
+libdir="$(env --unset=LD_PRELOAD ldd /sbin/cryptsetup | sed -nr '/.*=>\s*(\S+)\/libcryptsetup\.so\..*/ {s//\1/p;q}')"
+copy_exec "$libdir/ossl-modules/legacy.so" || true
+copy_libgcc "$libdir"
+
+for p in /sbin/cryptsetup /sbin/lvm /sbin/mdadm /sbin/mke2fs /sbin/mkfs.btrfs /bin/btrfs; do
+ if [ -x "$p" ]; then
+ copy_exec "$p"
+ fi
+done
+
+if [ "$BOOT" = "efi" ]; then
+ if [ ! -x "/sbin/mkfs.vfat" ]; then
+ echo "Couldn't find mkfs.vfat, is the 'dosfstools' package installed?" >&2
+ exit 1
+ fi
+ copy_exec /sbin/mkfs.vfat
+fi
+
+cp -pLt "$DESTDIR/lib" /lib/klibc-*.so
+for cmd in cat chroot ln ls mkdir mount mv sh umount uname; do
+ exe="/usr/lib/klibc/bin/$cmd"
+ if [ ! -f "$exe" ] || [ ! -x "$exe" ]; then
+ echo "No such executable: $exe" >&2
+ exit 1
+ fi
+ copy_exec "$exe" /bin
+done
+
+# copy udevd and (some of) its rules
+copy_exec /lib/systemd/systemd-udevd
+copy_exec /bin/udevadm
+
+mkdir -p -- "$DESTDIR/etc/udev" "$DESTDIR/lib/udev/rules.d"
+cat >"$DESTDIR/etc/udev/udev.conf" <<-EOF
+ udev_log=info
+ resolve_names=never
+EOF
+for rules in 50-udev-default.rules 55-dm.rules 60-block.rules \
+ 60-persistent-storage.rules 60-persistent-storage-dm.rules \
+ 63-md-raid-arrays.rules 95-dm-notify.rules; do
+ if [ -e "/lib/udev/rules.d/$rules" ]; then
+ cp -T "/lib/udev/rules.d/$rules" "$DESTDIR/lib/udev/rules.d/$rules"
+ fi
+done
+
+cd "$DESTDIR"
+find . -print0 | cpio -o0 -R 0:0 -H newc --quiet ${DEBUG:+--verbose} >"$INITRD"
diff --git a/debian/tests/utils/mock.pm b/debian/tests/utils/mock.pm
new file mode 100644
index 0000000..10db3e6
--- /dev/null
+++ b/debian/tests/utils/mock.pm
@@ -0,0 +1,347 @@
+# Mock terminal interaction on a guest system
+#
+# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+use v5.14.2;
+use warnings;
+use strict;
+
+our ($SERIAL, $CONSOLE, $MONITOR);
+our $PS1 = qr/root\@[\-\.0-9A-Z_a-z]+ : [~\/][\-\.\/0-9A-Z_a-z]* [\#\$]\ /aax;
+
+package CryptrootTest::Utils;
+
+use Socket qw/PF_UNIX SOCK_STREAM SOCK_CLOEXEC SOCK_NONBLOCK SHUT_RD SHUT_WR/;
+use Errno qw/EINTR ENOENT ECONNREFUSED/;
+use Time::HiRes ();
+
+my (%SOCKET, %BUFFER, $WBITS, $RBITS);
+
+BEGIN {
+ ($SERIAL, $CONSOLE, $MONITOR) = qw/ttyS0 hvc0 mon0/;
+ my $dir = $ARGV[1] =~ m#\A(/\p{Print}+)\z# ? $1 : die "Invalid base directory\n"; # untaint
+ my $epoch = Time::HiRes::time();
+ foreach my $id ($SERIAL, $CONSOLE, $MONITOR) {
+ my $path = $dir . "/" . $id;
+ my $sockaddr = Socket::pack_sockaddr_un($path) // die;
+ socket(my $socket, PF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) or die "socket: $!";
+
+ until (connect($socket, $sockaddr)) {
+ if ($! == EINTR) {
+ # try again immediatly if connect(2) was interrupted by a signal
+ } elsif (($! == ENOENT or $! == ECONNREFUSED) and Time::HiRes::time() - $epoch < 30) {
+ # wait a bit to give QEMU time to create the socket and mark it at listening
+ Time::HiRes::usleep(100_000);
+ } else {
+ die "connect($path): $!";
+ }
+ }
+
+ my $fd = fileno($socket) // die;
+ vec($WBITS, $fd, 1) = 1;
+ vec($RBITS, $fd, 1) = 1;
+ $SOCKET{$id} = $socket;
+ $BUFFER{$id} = "";
+ }
+}
+
+sub read_data($) {
+ my $bits = shift;
+ while (my ($chan, $fh) = each %SOCKET) {
+ next unless vec($bits, fileno($fh), 1); # nothing to read here
+ my $n = sysread($fh, my $buf, 4096) // die "read: $!";
+ if ($n > 0) {
+ STDOUT->printflush($buf);
+ $BUFFER{$chan} .= $buf;
+ } else {
+ #print STDERR "INFO done reading from $chan\n";
+ shutdown($fh, SHUT_RD) or die "shutdown: $!";
+ vec($RBITS, fileno($fh), 1) = 0;
+ }
+ }
+}
+
+sub expect(;$$) {
+ my ($chan, $prompt) = @_;
+
+ my $buffer = defined $chan ? \$BUFFER{$chan} : undef;
+ if (defined $buffer and $$buffer =~ $prompt) {
+ $$buffer = $' // die;
+ return %+;
+ }
+
+ while(unpack("b*", $RBITS) != 0) {
+ my $rout = $RBITS;
+ while (select($rout, undef, undef, undef) == -1) {
+ die "select: $!" unless $! == EINTR; # try again immediately if select(2) was interrupted
+ }
+ read_data($rout);
+ if (defined $buffer and $$buffer =~ $prompt) {
+ $$buffer = $' // die;
+ return %+;
+ }
+ }
+ #print STDERR "INFO done reading\n";
+}
+
+sub write_data($$%) {
+ my $chan = shift;
+ my $data = shift;
+
+ my %options = @_;
+ $options{echo} //= 1;
+ $options{eol} //= "\r";
+ $options{reol} //= "\r\n";
+ my $wdata = $data . $options{eol};
+
+ my $wfh = $SOCKET{$chan} // die;
+ my $wfd = fileno($wfh) // die;
+ vec(my $win, $wfd, 1) = 1;
+
+ for (my $offset = 0, my $length = length($wdata); $offset < $length;) {
+ my $wout = $win;
+ while (select(undef, $wout, undef, undef) == -1) {
+ die "select: $!" unless $! == EINTR; # try again immediately if select(2) was interrupted
+ }
+ if (vec($wout, $wfd, 1)) {
+ my $n = syswrite($wfh, $wdata, $length - $offset, $offset) // die "write: $!";
+ $offset += $n;
+ }
+ }
+
+ my $rdata = $options{echo} ? $data : "";
+ $rdata .= $options{reol};
+
+ if ($rdata ne "") {
+ my $buf = \$BUFFER{$chan};
+ my $rfh = $SOCKET{$chan} // die;
+ my $rfd = fileno($rfh) // die;
+ vec(my $rin, $rfd, 1) = 1;
+
+ my $rlen = length($rdata);
+ while($rlen > 0) {
+ my $rout = $rin;
+ while (select($rout, undef, undef, undef) == -1) {
+ die "select: $!" unless $! == EINTR; # try again immediately if select(2) was interrupted
+ }
+ read_data($rout);
+
+ my $got = substr($$buf, 0, $rlen);
+ my $n = length($got);
+ if ($got eq substr($rdata, -$rlen, $n)) {
+ $$buf = substr($$buf, $n); # consume the command
+ $rlen -= $n;
+ } else {
+ my $a = substr($rdata, 0, -$rlen) . substr($rdata, -$rlen, $n);
+ my $b = substr($rdata, 0, -$rlen) . $got;
+ s/[^\p{Graph} ]/"\\x".unpack("H*",$&)/ge foreach ($a, $b);
+ die "Wanted \"$a\", got \"$b\"";
+ }
+ }
+ }
+}
+
+package CryptrootTest::Mock;
+
+use Exporter qw/import/;
+BEGIN {
+ our @EXPORT = qw/
+ unlock_disk
+ login
+ shell
+ suspend
+ wakeup
+ hibernate
+ poweroff
+ expect
+ /;
+}
+
+*expect = \&CryptrootTest::Utils::expect;
+*write_data = \&CryptrootTest::Utils::write_data;
+
+sub unlock_disk($) {
+ my $passphrase = shift;
+ my %r = expect($SERIAL => qr/\A(?:.*(?:\r\n|\.\.\. ))?Please unlock disk (?<name>\p{Graph}+): \z/aams);
+ if ((my $ref = ref($passphrase)) ne "") {
+ my $name = $r{name};
+ unless (defined $name) {
+ undef $passphrase;
+ } elsif ($ref eq "CODE") {
+ $passphrase = $passphrase->($name);
+ } elsif ($ref eq "HASH") {
+ $passphrase = $passphrase->{$name};
+ } else {
+ die "Unsupported reference $ref";
+ }
+ }
+ die "Unable to unlock, aborting.\n" unless defined $passphrase;
+ write_data($SERIAL => $passphrase, echo => 0, reol => "\r");
+}
+
+sub login($;$) {
+ my ($username, $password) = @_;
+ expect($CONSOLE => qr/\r\ncryptroot-[[:alnum:]._-]+ login: \z/aams);
+ write_data($CONSOLE => $username, reol => "\r");
+
+ if (defined $password) {
+ expect($CONSOLE => qr/\A[\r\n]*Password: \z/aams);
+ write_data($CONSOLE => $username, echo => 0, reol => "\r");
+ }
+
+ # consume motd(5) or similar
+ expect($CONSOLE => qr/\r\n $PS1 \z/aamsx);
+}
+
+sub shell($%);
+sub shell($%) {
+ my $command = shift;
+ my %options = @_;
+
+ write_data($CONSOLE => $command);
+ my %r = expect($CONSOLE => qr/\A (?<out>.*) $PS1 \z/aamsx);
+ my $out = $r{out};
+
+ if (exists $options{rv}) {
+ my $rv = shell(q{echo $?});
+ unless ($rv =~ s/\r?\n\z// and $rv =~ /\A[0-9]+\z/ and $rv == $options{rv}) {
+ my @loc = caller;
+ die "ERROR: Command \`$command\` exited with status $rv != $options{rv}",
+ " at line $loc[2] in $loc[1]\n";
+ }
+ }
+ return $out;
+}
+
+# enter S3 sleep state (suspend to ram aka standby)
+sub suspend() {
+ write_data($CONSOLE => q{systemctl suspend});
+ # while the command is asynchronous the system might suspend before
+ # we have a chance to read the next $PS1
+
+ # wait for the SUSPEND event
+ QMP::wait_for_event("SUSPEND");
+
+ # double check that the guest is indeed suspended
+ my $resp = QMP::command(q{query-status});
+ die unless defined $resp->{status} and $resp->{status} eq "suspended" and
+ defined $resp->{running} and $resp->{running} == JSON::false();
+}
+
+sub wakeup() {
+ my $r = QMP::command(q{system_wakeup});
+ die if %$r;
+
+ # wait for the WAKEUP event
+ QMP::wait_for_event("WAKEUP");
+
+ # double check that the guest is indeed running
+ my $resp = QMP::command(q{query-status});
+ die unless defined $resp->{status} and $resp->{status} eq "running" and
+ defined $resp->{running} and $resp->{running} == JSON::true();
+}
+
+# enter S4 sleep state (suspend to disk aka hibernate)
+sub hibernate() {
+ # an alternative is to send {"execute":"guest-suspend-disk"} on the
+ # guest agent socket, but we don't want to require qemu-guest-agent
+ # on the guest so this will have to do
+ write_data($CONSOLE => q{systemctl hibernate});
+ # while the command is asynchronous the system might hibernate
+ # before we have a chance to read the next $PS1
+ QMP::wait_for_event("SUSPEND_DISK");
+ expect();# wait for QEMU to terminate
+}
+
+sub poweroff() {
+ # XXX would be nice to use the QEMU monitor here but the guest
+ # doesn't seem to respond to system_powerdown QMP commands
+ write_data($CONSOLE => q{poweroff});
+ # while the command is asynchronous the system might shutdown
+ # before we have a chance to read the next $PS1
+ QMP::wait_for_event("SHUTDOWN");
+ expect(); # wait for QEMU to terminate
+}
+
+
+package QMP;
+
+# QMP protocol
+# https://qemu.readthedocs.io/en/latest/interop/qemu-qmp-ref.html
+
+use JSON ();
+
+# read and decode a QMP server line
+sub getline() {
+ my %r = CryptrootTest::Utils::expect($MONITOR => qr/\A(?<str>.+?)\r\n/m);
+ my $str = $r{str} // die;
+ return JSON::->new->decode($str);
+}
+
+# send a QMP command and optional arguments
+sub command($;$) {
+ my ($command, $arguments) = @_;
+ my $cmd = { execute => $command };
+ $cmd->{arguments} = $arguments if defined $arguments;
+
+ $cmd = JSON::->new->encode($cmd);
+ STDOUT->printflush($cmd . "\n");
+ CryptrootTest::Utils::write_data($MONITOR => $cmd, eol => "\r\n", echo => 0, reol => "");
+
+ while(1) {
+ my $resp = QMP::getline() // next;
+ # ignore unsolicited server responses (such as events)
+ return $resp->{return} if exists $resp->{return};
+ }
+}
+
+# wait for the QMP greeting line
+my @CAPABILITIES;
+sub greeting() {
+ my $greeting = QMP::getline() // die;
+ $greeting = $greeting->{QMP} // die;
+ @CAPABILITIES = @{$greeting->{capabilities}} if defined $greeting->{capabilities};
+}
+
+# negotiate QMP capabilities
+sub capabilities(@) {
+ my $r = QMP::command(qmp_capabilities => {enable => \@_});
+ die if %$r;
+}
+
+BEGIN {
+ # https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qmp-spec.txt sec 4
+ QMP::greeting();
+ QMP::capabilities();
+}
+
+sub wait_for_event($) {
+ my $event_name = shift;
+ while(1) {
+ my $resp = QMP::getline() // next;
+ return if exists $resp->{event} and $resp->{event} eq $event_name;
+ }
+}
+
+sub quit() {
+ # don't use QMP::command() here since we might never receive a response
+ my $cmd = JSON::->new->encode({ execute => "quit" });
+ STDOUT->printflush($cmd . "\n");
+ CryptrootTest::Utils::write_data($MONITOR => $cmd, eol => "\r\n", echo => 0, reol => "");
+ CryptrootTest::Utils::expect(); # wait for QEMU to terminate
+}
+
+1;