Bounded waits and validated arguments in run.sh and ui-drive.sh, a seeded settings directory and root-only staged rootfs permissions with their own tests, qmp.py and imgtools.py hardening, the fetch scripts checking what they download, and ASCII typography throughout. Each fix carries its test under qemu/tests or tests/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
191 lines
8.2 KiB
Bash
Executable File
191 lines
8.2 KiB
Bash
Executable File
#!/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 ]
|