#!/bin/bash # ===================================================================== # The freeze must happen, and must happen first. # # ./tests/test_vm_freeze.sh # # WHY THIS FILE EXISTS # # Freezing a base image is one `chmod 0444`, which makes it look like # the kind of thing that cannot go wrong. It went wrong: a later line in # the same function used `du --apparent-size=0`, which is not valid - # --apparent-size takes no argument - and under `set -euo pipefail` that # killed the script before the chmod ran. Every clean shutdown left the # base image WRITABLE, and the only sign was # "Control process exited, status=1/FAILURE" in the journal. # # So this runs the real script against a scratch directory and checks # the mode afterwards, rather than checking that the line is present. # ===================================================================== set -uo pipefail PREPARE=/usr/local/libexec/owuivm-maint-prepare fails=0 ok() { printf ' ok %s\n' "$*"; } bad() { printf ' FAIL %s\n' "$*"; fails=$((fails+1)); } work=$(mktemp -d); trap 'rm -rf "$work"' EXIT base="$work/base"; isos="$work/iso" mkdir -p "$base" "$isos" # EVERY DIRECTORY THIS TOUCHES IS ITS OWN, INCLUDING THE RUN DIRECTORY. # # It used to use the real /run/owuivm/. Run as root, # that left a root:root directory behind, and the operator's next # maintenance boot died in ExecStartPre on "cp: cannot remove # .../OVMF_VARS.fd: Permission denied" - a tool that looked broken # because a test had littered. run_dir="$work/run" mkdir -p "$run_dir" run_verb() { OWUIVM_BASE_DIR="$base" OWUIVM_ISO_DIR="$isos" OWUIVM_OVERLAY_DIR="$work" \ OWUIVM_RUN_DIR="$run_dir" \ "$PREPARE" ubuntu "$1" 2>&1 } # `close` is what the unit runs on the way out; `freeze` and `thaw` are # the operator's own buttons. They used to be one verb, which is why # looking at an image meant unfreezing it. close() { run_verb close; } freeze() { run_verb freeze; } thaw() { run_verb thaw; } echo "== a disk with an OS on it" # 600 MB: over the "something was installed" threshold. dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd" chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd" echo "$isos/ubuntu-x.iso" > "$base/ubuntu.install" out=$(freeze) mode=$(stat -c %a "$base/ubuntu.qcow2") [ "$mode" = "444" ] && ok "the disk is frozen (0444)" || bad "the disk is $mode, not 444 — $out" out=$(close) # The marker is SUPPOSED to survive now. It used to be removed once the # disk passed half a gigabyte, which declared a partial install finished, # took its media away, and left a disk that was neither blank enough to # restart nor complete enough to boot. Size cannot tell "installed" from # "interrupted halfway", so nothing guesses: the boot order in # owuivm-launch tries the disk first and falls through to the media, and # detaching it is an explicit act by the operator. [ -f "$base/ubuntu.install" ] && ok "the install marker survives a freeze (nothing guesses)" \ || bad "the install marker was removed - a partial install cannot be resumed" [ -f "$base/ubuntu.sha256" ] && ok "a checksum was recorded" || bad "no checksum recorded" echo echo "== a disk nothing was installed on yet" rm -rf "$base"/*; mkdir -p "$base" dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=1 status=none cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd" chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd" echo "$isos/ubuntu-x.iso" > "$base/ubuntu.install" out=$(freeze) mode=$(stat -c %a "$base/ubuntu.qcow2") [ "$mode" = "444" ] && ok "the disk is frozen even when nothing was installed" \ || bad "the disk is $mode, not 444 — $out" out=$(close) # An interrupted install must be resumable; removing the marker here is # what left a blank disk with no installer attached and no way back. [ -f "$base/ubuntu.install" ] && ok "the install marker survived an interrupted install" \ || bad "the install marker was removed after an interrupted install" echo echo "== firmware variables are only kept when there is an OS to keep them for" rm -rf "$base"/*; mkdir -p "$base" dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=1 status=none printf 'ORIGINAL' > "$base/ubuntu-OVMF_VARS.fd" chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd" if true; then printf 'REWRITTEN-BY-A-BOOT-THAT-INSTALLED-NOTHING' > "$run_dir/OVMF_VARS.fd" close >/dev/null if grep -q ORIGINAL "$base/ubuntu-OVMF_VARS.fd" 2>/dev/null; then ok "a boot that installed nothing did not rewrite the frozen firmware variables" else bad "firmware variables from an empty boot were carried into the base image" fi rm -f "$run_dir/OVMF_VARS.fd" fi echo echo "== the freeze survives a broken step after it" rm -rf "$base"/*; mkdir -p "$base" dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd" chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd" # A directory where a file is expected: the checksum step will fail. mkdir -p "$base/ubuntu.sha256.previous" out=$(freeze) mode=$(stat -c %a "$base/ubuntu.qcow2") [ "$mode" = "444" ] && ok "still frozen despite a later step failing" \ || bad "a failure after the chmod undid the freeze — $out" echo echo "== a frozen boot leaves nothing behind" # The point of the whole split: the operator can start a base image to # see that it still comes up, exactly as the agent will get it, and # being there cannot change it. OVMF rewrites its boot order on every # boot, so "unchanged" has to include the firmware variables. rm -rf "$base"/*; mkdir -p "$base" dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none printf 'ORIGINAL-FIRMWARE' > "$base/ubuntu-OVMF_VARS.fd" chmod 0444 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd" disk_sum=$(sha256sum "$base/ubuntu.qcow2" | cut -d' ' -f1) touch "$work/maint-ubuntu.qcow2" if [ -d "$run_dir" ] || mkdir -p "$run_dir" 2>/dev/null; then printf 'REWRITTEN-BY-A-LOOK-AT-IT-BOOT' > "$run_dir/OVMF_VARS.fd" out=$(close) grep -q ORIGINAL-FIRMWARE "$base/ubuntu-OVMF_VARS.fd" \ && ok "a frozen boot did not rewrite the firmware variables" \ || bad "a frozen boot changed the base image's firmware — $out" [ "$(sha256sum "$base/ubuntu.qcow2" | cut -d' ' -f1)" = "$disk_sum" ] \ && ok "the base image is byte-identical after a frozen boot" \ || bad "the base image changed during a frozen boot" [ -f "$work/maint-ubuntu.qcow2" ] \ && bad "the throwaway disk survived the boot - its changes would come back" \ || ok "the throwaway disk was removed" rm -f "$run_dir/OVMF_VARS.fd" fi echo echo "== the operator's own verbs, which nothing else may do for them" rm -rf "$base"/*; mkdir -p "$base" dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd" chmod 0444 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd" out=$(thaw) [ "$(stat -c %a "$base/ubuntu.qcow2")" = "660" ] \ && ok "thaw makes the image writable" || bad "thaw left it $(stat -c %a "$base/ubuntu.qcow2") — $out" # And `close` must NOT put it back. That auto-freeze is what made # looking at an image mean unfreezing it, and it hid a forgotten thaw; # the refusal now lives in vmd, where a session cannot start without it. out=$(close) [ "$(stat -c %a "$base/ubuntu.qcow2")" = "660" ] \ && ok "closing an unfrozen boot leaves it unfrozen, and says so" \ || bad "close re-froze the image behind the operator — $out" case "$out" in *WRITABLE*) ok "and the log says the image is still writable" ;; *) bad "close said nothing about leaving a writable image: $out" ;; esac out=$(freeze) [ "$(stat -c %a "$base/ubuntu.qcow2")" = "444" ] \ && ok "freeze puts it back" || bad "freeze left it $(stat -c %a "$base/ubuntu.qcow2") — $out" [ -f "$base/ubuntu.sha256" ] && ok "and re-records the checksum" || bad "no checksum after freeze" echo echo "== the desktop tool starts units and touches nothing" # WHY: the tool created the blank disk, the firmware variables and the # install marker itself, as the operator. Pressing "Install from ISO" for # Windows failed with a bare permission error, because `operator` is a # member of owuivmadm in /etc/group and the desktop SESSION predates the # usermod - a unix group only reaches a process at its next login, so the # group exists, `id` agrees, and nothing running has it. # # It was wrong a second way that would have surfaced much later: it # copied the plain OVMF variables for both images, where Windows needs # the store with Microsoft's keys enrolled or Secure Boot has nothing it # trusts and setup refuses the machine. gui=/usr/local/bin/owuivm-maintenance for forbidden in "qemu-img" "shutil.copyfile" "OVMF_VARS ="; do grep -q "$forbidden" "$gui" 2>/dev/null \ && bad "the desktop tool still does '$forbidden' itself - it cannot, and it gets it wrong" \ || ok "the desktop tool does not do '$forbidden' itself" done echo echo "== what the unit makes for a brand new image" rm -rf "$base"/*; mkdir -p "$base" for name in ubuntu windows; do out=$(OWUIVM_BASE_DIR="$base" OWUIVM_ISO_DIR=/var/lib/owuivm/iso \ OWUIVM_OVERLAY_DIR="$work" OWUIVM_RUN_DIR="$run_dir" \ "$PREPARE" "$name" open 2>&1) if [ ! -f "$base/$name.qcow2" ]; then printf ' skip %s: no media in /var/lib/owuivm/iso (%s)\n' "$name" "${out##*$'\n'}" continue fi ok "$name: a blank disk was created" [ $(( 8#$(stat -c %a "$base/$name.qcow2") & 8#200 )) -ne 0 ] \ && ok "$name: and it is writable, because an install has to write to it" \ || bad "$name: the new disk is read-only, so the installer cannot use it" grep -q '^/var/lib/owuivm/iso/' "$base/$name.install" 2>/dev/null \ && ok "$name: the media it will boot is recorded ($(basename "$(cat "$base/$name.install")"))" \ || bad "$name: no install marker, so the installer is never attached" # The one difference between the two images that is easy to get wrong # and impossible to see until Windows setup refuses the machine. case "$name" in windows) want=/usr/share/OVMF/OVMF_VARS_4M.ms.fd label="the store with Microsoft's keys enrolled" ;; *) want=/usr/share/OVMF/OVMF_VARS_4M.fd label="the plain store" ;; esac cmp -s "$base/$name-OVMF_VARS.fd" "$want" \ && ok "$name: firmware variables are $label" \ || bad "$name: firmware variables are NOT $label - Secure Boot will not behave" done rm -f "$base"/*.install echo echo "== and the operator can actually do that explicit act" # The line above says detaching the media is the operator's decision. # It was a sentence in a comment for a while and nothing more: the # operator's only way to make it was to delete a file in a directory # their own account cannot write. These four check that the control # exists, that it is reachable, and that it does one thing. unit=/etc/systemd/system/owuivm-detach@.service if [ -f "$unit" ]; then ok "there is a unit for it" exec_line=$(sed -n 's/^ExecStart=//p' "$unit") case "$exec_line" in *"rm -f /var/lib/owuivm/base/%i.install") ok "it removes the marker and nothing else" ;; *) bad "the detach unit does more than remove the marker: $exec_line" ;; esac grep -q '^ConditionPathExists=/var/lib/owuivm/base/%i.install' "$unit" \ && ok "pressing it twice is not an error" \ || bad "no ConditionPathExists - a second press reports a failure" else bad "no owuivm-detach@.service - detaching the media is still a shell command" fi for verb in thaw freeze; do u="/etc/systemd/system/owuivm-$verb@.service" [ -f "$u" ] && ok "there is a unit for $verb" \ || bad "no owuivm-$verb@.service - the operator cannot $verb an image" grep -q "owuivm-$verb@" /etc/polkit-1/rules.d/50-owuivm.rules \ && ok "polkit authorises $verb" \ || bad "owuivm-$verb@ is not in the polkit rule, so the desktop tool cannot start it" done # One button, two verbs, and the label has to be the state - a toggle # that does not say which way it points is worse than two buttons. grep -q 'owuivm-{verb}@{name}.service' /usr/local/bin/owuivm-maintenance 2>/dev/null \ && ok "the maintenance tool has the freeze/unfreeze control" \ || bad "the desktop tool cannot freeze or unfreeze anything" grep -q '"Unfreeze" if state\["frozen"\] else "Freeze"' /usr/local/bin/owuivm-maintenance 2>/dev/null \ && ok "and its label is the image's state" \ || bad "the toggle does not say which way it points" grep -q 'owuivm-detach@' /etc/polkit-1/rules.d/50-owuivm.rules \ && ok "polkit authorises it for an active session of the operator" \ || bad "the detach unit is not in the polkit rule, so the desktop tool cannot start it" # The service without a button is the same sentence in a different file. gui=/usr/local/bin/owuivm-maintenance grep -q 'owuivm-detach@{name}.service' "$gui" 2>/dev/null \ && ok "the maintenance tool has a control wired to it" \ || bad "the desktop tool still has no way to press it" # The one thing pressing it early would cost: a machine shut down # mid-install with no disc to go back to. grep -q 'state\["installing"\] and not state\["running"\]' "$gui" 2>/dev/null \ && ok "the control is dead while the machine is running" \ || bad "the control can be pressed mid-install" echo if [ "$fails" -gt 0 ]; then echo "FAILED $fails"; exit 1; fi echo "the freeze holds"