utils/vmbox/bin/vm
Krzysztof kuhy Rudnicki 00bb6706fd
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
vmbox: vm lightdm and vm shim — the host's login topology and fake hardware
The golden image logs in with agetty+startx and has no RGB controllers,
no NVIDIA card and no sound card, so the night-lockdown scripts — which
mask/stop lightdm.service and getty@.service and shell out to openrgb,
nvidia-smi and amixer — had nothing real to act on in a sandbox and
were being "verified" on the host instead.

- `vm lightdm <name>`: installs lightdm + lightdm-gtk-greeter into the
  live overlay (pacman -Syu, not -S: the overlay's package database is
  the image's and mirrors have rotated those versions out), configures
  autologin -> i3 exactly like the host, turns tty1 back into a plain
  getty, reboots the guest and proves lightdm.service is active with an
  Xorg it owns. Never rebuilds the base: `vm build --force` re-seals a
  new checksum while every existing sandbox still backs onto the old
  file's name, which is silent qcow2 corruption.
- `vm shim <name> <tool> [exit[:N]|hang|sleep:S]`: a fake tool ahead of
  the real one on PATH, every call logged with an epoch to
  /var/log/vmbox-shim/<tool>.log so the host can assert what ran and in
  what order. `hang` is the openrgb 1.0-2 that never returned on
  2026-09-12 and defeated the curfew.
- The DISPLAY/XAUTHORITY resolver `vm run` sources becomes its own file,
  guest/vmbox-x11.sh, installed by the image build and re-installed by
  the lightdm switch; it now tests readability, not existence, and tries
  ~/.Xauthority before the startx serverauth glob, because lightdm's
  Xorg -auth file is root-only.
- README: the worked example uses MIDNIGHT_SHUTDOWN_CONFIRM=y (the
  installer refuses to run unattended now; `echo y |` no longer works),
  and the "i2c RGB stays on the host" line is gone.
- bats: four host-side tests for shim.sh (exit, exit:N, hang, refusals).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mLnUeoJ3mUwGRTowTUH6d
2026-09-13 10:51:10 +02:00

114 lines
5.2 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# vm -- disposable Arch VM sandbox.
#
# Runs destructive system scripts (shutdown installers, chattr +i guards,
# pacman hooks) against a throwaway Arch guest, and verifies the outcome from
# the HOST -- a guest that powers itself off cannot report its own success.
# ============================================================================
set -euo pipefail
VMBOX_BIN_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd -P)"
readonly VMBOX_BIN_DIR
# shellcheck source=../lib/common.sh
source "$VMBOX_BIN_DIR/../lib/common.sh"
usage() {
cat <<'USAGE'
Usage: vm <command> [args]
build Build the golden base image (once, ~10 min)
new <name> [--rtc <ts>] Create a sandbox (instant; thin overlay)
run <name> <command...> Run a command in the sandbox, report the verdict
ssh <name> [args...] Interactive shell in the sandbox
scp <name> <src> <dest> Copy a host file into the sandbox
share <host-path> Expose a host dir to sandboxes (read-only bind)
unshare <name> Stop exposing it
repo <name> <path> [--worktree]
Copy a host repo in (clone=HEAD, --worktree=uncommitted)
screenshot <name> [out.png] Capture the guest screen (for locker tests)
lightdm <name> Switch the sandbox to lightdm autologin -> i3,
tty1 a plain getty (the host's login topology)
shim <name> <tool> [mode] Fake a hardware tool on PATH: exit[:N] | hang |
sleep:S; calls logged in /var/log/vmbox-shim/
reset <name> Wipe back to pristine (sub-second)
rm <name> Delete the sandbox entirely
list List sandboxes and their state
--rtc <ts> Pin the guest clock, e.g. 2026-08-22T20:55:00. Stored in the
VM's meta and re-applied on EVERY launch: the shutdown scripts
are gated on a 21:00-05:00 window, so an unpinned clock makes
their tests nondeterministic.
Examples:
vm new demo --rtc 2026-08-22T20:55:00
vm run demo 'sudo systemctl poweroff' # -> verdict: clean poweroff
vm reset demo # back to pristine
USAGE
}
# vm repo <name> <path> [--worktree]
cmd_repo() {
local name="${1:-}" src="${2:-}" mode=clone
[[ -n "$name" && -n "$src" ]] || die "usage: vm repo <name> <host-path> [--worktree]"
shift 2
[[ "${1:-}" == "--worktree" ]] && mode=worktree
repo_sync "$name" "$src" "$mode"
}
# Copy a file into a sandbox. lib/ssh.sh has always been able to do this;
# without a subcommand the only route was base64 through `vm run`.
cmd_scp() {
local name="${1:-}" src="${2:-}" dest="${3:-}"
[[ -n "$name" && -n "$src" && -n "$dest" ]] ||
die "usage: vm scp <name> <host-src> <guest-dest>"
name="$(validate_vm_name "$name")"
require_vm "$name"
[[ -e "$src" ]] || die "no such file: $src"
vm_is_running "$name" || die "sandbox '$name' is not running (start it with: vm run $name true)"
vm_scp_to "$name" "$src" "$dest" || die "copy failed"
ok "copied $src -> $name:$dest"
}
cmd_list() {
[[ -d "$VMBOX_VMS_DIR" ]] || { echo "no sandboxes yet (create one: vm new <name>)"; return 0; }
local found=0 d name state rtc
printf '%-20s %-12s %s\n' "NAME" "STATE" "RTC"
for d in "$VMBOX_VMS_DIR"/*/; do
[[ -d "$d" ]] || continue
found=1
name="$(basename "$d")"
if vm_is_running "$name"; then state="running"; else state="stopped"; fi
rtc="$(meta_get "$name" rtc 2>/dev/null || echo '-')"
printf '%-20s %-12s %s\n' "$name" "$state" "${rtc:--}"
done
(( found )) || echo "(none)"
}
main() {
local cmd="${1:-}"
[[ $# -gt 0 ]] && shift || true
case "$cmd" in
build) require_host_deps; source "$VMBOX_LIB_DIR/base.sh"; base_build "$@" ;;
new) require_host_deps; source "$VMBOX_LIB_DIR/overlay.sh"; overlay_new "$@" ;;
reset) source "$VMBOX_LIB_DIR/overlay.sh"; overlay_reset "$@" ;;
rm) source "$VMBOX_LIB_DIR/overlay.sh"; overlay_rm "$@" ;;
run) require_host_deps; source "$VMBOX_LIB_DIR/run.sh"; run_in_vm "$@" ;;
ssh) source "$VMBOX_LIB_DIR/ssh.sh"; vm_ssh_interactive "$@" ;;
scp) source "$VMBOX_LIB_DIR/ssh.sh"; cmd_scp "$@" ;;
screenshot) source "$VMBOX_LIB_DIR/screenshot.sh"; vm_screenshot "$@" ;;
lightdm) require_host_deps; source "$VMBOX_LIB_DIR/desktop.sh"; cmd_lightdm "$@" ;;
shim) require_host_deps; source "$VMBOX_LIB_DIR/desktop.sh"; cmd_shim "$@" ;;
repo) source "$VMBOX_LIB_DIR/repo.sh"; cmd_repo "$@" ;;
share) source "$VMBOX_LIB_DIR/repo.sh"; repo_share "$@" ;;
unshare) source "$VMBOX_LIB_DIR/repo.sh"; repo_unshare "$@" ;;
list|ls) cmd_list ;;
-h|--help|help|'') usage ;;
*) die "unknown command '$cmd' (try: vm --help)" ;;
esac
}
main "$@"