#!/usr/bin/env bash # Regression test for mkimage.sh's SEED_DIR hook (see its Env note and the # block right after --state is applied): a caller-supplied directory of # pre-built userdata/warden files, copied in whole and applied AFTER --state # so a seeded file can override a same-named --state value, after each entry # is validated the same way --state's own KEY=VALUE is validated -- a # symlink, a non-plain-file entry (a subdirectory included), or a name # outside [A-Za-z0-9_.-]+ fails closed before cp -a runs. Nothing else in # the qemu test suite ever sets SEED_DIR -- the CI qemu-tools job runs # mkimage.sh unseeded, and only ui-drive.sh --seed exercises this path, and # only when booting a real VM with a flare-edge checkout on hand -- so this # is the only offline coverage of it. # # Builds a real disk image the same way mkimage.sh always does (unprivileged # mkfs.ext4 -d), then reads the userdata partition back with debugfs -R # (read-only, no mount or loop device needed) to check what actually landed # on disk rather than trusting the script's own log output. # # bash seed-dir.sh set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/ QEMU_DIR="$(cd "$HERE/.." && pwd)" # qemu/ FAIL=0 pass() { printf '[PASS] %s\n' "$1"; } fail() { printf '[FAIL] %s\n' "$1"; FAIL=1; } # mkfs.ext4 and debugfs both live in sbin, which user shells on Debian don't # put on PATH -- same fix mkimage.sh itself applies. PATH="$PATH:/usr/sbin:/sbin" command -v debugfs >/dev/null || { echo "FATAL: debugfs (e2fsprogs) not found: needed to read the userdata partition back" >&2 exit 1 } # shellcheck source=../lib.sh disable=SC1091 . "$QEMU_DIR/lib.sh" # shellcheck source=../blkdevparts.conf disable=SC1091 . "$QEMU_DIR/blkdevparts.conf" SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/warden-qemu-seed-dir.XXXXXX")" trap 'rm -rf "$SCRATCH"' EXIT # userdata's byte offset/size come from the same blkdevparts string # mkimage.sh itself parses, not a hardcoded number: a future layout change # doesn't strand this test. USERDATA_OFF="" USERDATA_SIZE="" capture_userdata() { [ "$1" = userdata ] && { USERDATA_OFF="$2"; USERDATA_SIZE="$3"; }; return 0; } qemu_each_partition capture_userdata [ -n "$USERDATA_OFF" ] || { echo "FATAL: no 'userdata' entry in blkdevparts.conf" >&2; exit 1; } # extract_userdata DISK OUTFILE: pull the userdata partition window out of a # built disk image. Sparse output so an otherwise near-empty 1G partition # costs kilobytes of scratch space, not a real gigabyte, per scenario. extract_userdata() { dd if="$1" of="$2" bs=4096 skip=$((USERDATA_OFF / 4096)) \ count=$((USERDATA_SIZE / 4096)) conv=sparse status=none } # Reuse an already-verified busybox (read-only) so this test stays offline # wherever a prior build has already produced one; only a checkout that has # never run mkimage.sh falls back to the same fetch+verify mkimage.sh always # does, once, shared by every scenario below. BUSYBOX_BIN="$QEMU_DIR/out/busybox-armv7l" if [ ! -f "$BUSYBOX_BIN" ]; then OUT="$SCRATCH" qemu_get_busybox BUSYBOX_BIN="$BB" fi # --- scenario 1: seed applied verbatim, secret mode preserved, seed beats a same-named --state --- SEED="$SCRATCH/seed" mkdir -p "$SEED" printf 'plain-value\n' > "$SEED/plain.key" printf 'secret-value\n' > "$SEED/secret.key" chmod 0600 "$SEED/secret.key" printf 'seeded-value\n' > "$SEED/override.key" OUT1="$SCRATCH/out1" if OUT="$OUT1" BUSYBOX="$BUSYBOX_BIN" SEED_DIR="$SEED" \ bash "$QEMU_DIR/mkimage.sh" --state "override.key=state-value" \ > "$SCRATCH/mkimage1.log" 2>&1; then UIMG="$SCRATCH/userdata1.img" extract_userdata "$OUT1/disk.img" "$UIMG" plain_stat="$(debugfs -R "stat /warden/plain.key" "$UIMG" 2>/dev/null)" if [ -n "$plain_stat" ]; then pass "SEED_DIR: plain.key landed under userdata/warden" else fail "SEED_DIR: plain.key missing from userdata/warden" fi secret_stat="$(debugfs -R "stat /warden/secret.key" "$UIMG" 2>/dev/null)" secret_mode="$(printf '%s' "$secret_stat" | grep -oE 'Mode: *[0-7]+' | grep -oE '[0-7]+$')" if [ "$secret_mode" = "0600" ]; then pass "SEED_DIR: secret.key kept mode 0600 through cp -a" else fail "SEED_DIR: secret.key mode '$secret_mode', want 0600" fi override_content="$(debugfs -R "cat /warden/override.key" "$UIMG" 2>/dev/null)" if [ "$override_content" = "seeded-value" ]; then pass "SEED_DIR: seeded override.key beats the same-named --state value" else fail "SEED_DIR: override.key = '$override_content', want 'seeded-value' (seed must apply after --state)" fi else fail "SEED_DIR: mkimage.sh exited nonzero with a valid seed dir (see $SCRATCH/mkimage1.log)" fi # --- scenario 2: SEED_DIR that is not a directory fails closed --- NOTADIR="$SCRATCH/notadir" : > "$NOTADIR" OUT2="$SCRATCH/out2" err2="$(OUT="$OUT2" BUSYBOX="$BUSYBOX_BIN" SEED_DIR="$NOTADIR" \ bash "$QEMU_DIR/mkimage.sh" 2>&1 1>/dev/null)" rc2=$? if [ "$rc2" -ne 0 ] && printf '%s' "$err2" | grep -qF "FATAL: SEED_DIR '$NOTADIR' is not a directory"; then pass "SEED_DIR: a non-directory path fails closed with the FATAL message" else fail "SEED_DIR: non-directory path gave rc=$rc2, stderr='$err2' (want nonzero + the FATAL message)" fi # --- scenario 3: a symlink entry fails closed instead of being copied verbatim --- SEED3="$SCRATCH/seed3" mkdir -p "$SEED3" printf 'plain-value\n' > "$SEED3/plain.key" ln -s /etc/passwd "$SEED3/evil.key" OUT3="$SCRATCH/out3" err3="$(OUT="$OUT3" BUSYBOX="$BUSYBOX_BIN" SEED_DIR="$SEED3" \ bash "$QEMU_DIR/mkimage.sh" 2>&1 1>/dev/null)" rc3=$? if [ "$rc3" -ne 0 ] && printf '%s' "$err3" | grep -qF "FATAL: SEED_DIR entry 'evil.key' is a symlink"; then pass "SEED_DIR: a symlink entry fails closed instead of being copied verbatim" else fail "SEED_DIR: symlink entry gave rc=$rc3, stderr='$err3' (want nonzero + the symlink FATAL message)" fi [ -e "$OUT3/disk.img" ] && fail "SEED_DIR: a disk image was written despite the symlink entry" # --- scenario 4: an entry with a character outside [A-Za-z0-9_.-]+ fails closed --- SEED4="$SCRATCH/seed4" mkdir -p "$SEED4" printf 'x\n' > "$SEED4/bad key" OUT4="$SCRATCH/out4" err4="$(OUT="$OUT4" BUSYBOX="$BUSYBOX_BIN" SEED_DIR="$SEED4" \ bash "$QEMU_DIR/mkimage.sh" 2>&1 1>/dev/null)" rc4=$? if [ "$rc4" -ne 0 ] && printf '%s' "$err4" | grep -qF "FATAL: SEED_DIR entry 'bad key' must match [A-Za-z0-9_.-]+"; then pass "SEED_DIR: an entry name outside [A-Za-z0-9_.-]+ fails closed" else fail "SEED_DIR: bad-name entry gave rc=$rc4, stderr='$err4' (want nonzero + the charset FATAL message)" fi # --- scenario 5: a subdirectory entry fails closed (not a plain file) --- SEED5="$SCRATCH/seed5" mkdir -p "$SEED5/subdir" printf 'x\n' > "$SEED5/subdir/leaf.key" OUT5="$SCRATCH/out5" err5="$(OUT="$OUT5" BUSYBOX="$BUSYBOX_BIN" SEED_DIR="$SEED5" \ bash "$QEMU_DIR/mkimage.sh" 2>&1 1>/dev/null)" rc5=$? if [ "$rc5" -ne 0 ] && printf '%s' "$err5" | grep -qF "FATAL: SEED_DIR entry 'subdir' is not a plain file"; then pass "SEED_DIR: a subdirectory entry fails closed instead of being recursed into" else fail "SEED_DIR: subdirectory entry gave rc=$rc5, stderr='$err5' (want nonzero + the plain-file FATAL message)" fi # --- scenario 6: a hyphenated key (seed-fixtures.py's KEY_RE, e.g. # "gas-plant.devices") still seeds cleanly -- guards against tightening the # charset to --state's stricter [A-Za-z0-9_.]+ by mistake, which would # reject keys committed flow specs already seed through this path --- SEED6="$SCRATCH/seed6" mkdir -p "$SEED6" printf 'r5\n' > "$SEED6/gas-plant.devices" OUT6="$SCRATCH/out6" if OUT="$OUT6" BUSYBOX="$BUSYBOX_BIN" SEED_DIR="$SEED6" \ bash "$QEMU_DIR/mkimage.sh" > "$SCRATCH/mkimage6.log" 2>&1; then UIMG6="$SCRATCH/userdata6.img" extract_userdata "$OUT6/disk.img" "$UIMG6" hyphen_content="$(debugfs -R "cat /warden/gas-plant.devices" "$UIMG6" 2>/dev/null)" if [ "$hyphen_content" = "r5" ]; then pass "SEED_DIR: a hyphenated key (gas-plant.devices) still seeds cleanly" else fail "SEED_DIR: gas-plant.devices = '$hyphen_content', want 'r5'" fi else fail "SEED_DIR: mkimage.sh rejected a valid hyphenated key (see $SCRATCH/mkimage6.log)" fi [ "$FAIL" -eq 0 ] && echo "ALL SEED_DIR TESTS PASSED" || echo "SEED_DIR TESTS FAILED" [ "$FAIL" -eq 0 ]