End-to-end now drives a real KVM guest through the full XMRig-shaped
phase schedule with the workload running INSIDE the guest. Telemetry is
host-side /proc/<qemu_pid>; the load is busybox `yes` (sustained CPU
saturation) and `dd if=/dev/urandom` (disk burst on infecting), driven
over the serial console at every phase transition. The plotted envelope
shows clean idle → armed → infecting (disk spike) → infected_running
(100% CPU plateau) → dormant → re-entry → final clean.
Components:
vm/launch_demo.sh now boots Alpine 3.21 nocloud-cloudinit
(Cirros 0.6.x's cirros-init blocks on the
EC2 metadata service for ~17 min before
falling through to NoCloud — abandoned).
Mounts a cidata ISO as a second drive.
tools/build_cidata.py pure-Python NoCloud ISO builder (pycdlib).
Sets root password and ssh_pwauth via
runcmd so we don't depend on a specific
cloud-init version's plain_text_passwd
handling.
tools/vm_serial.py serial-console client (stdlib socket).
Idempotent login (detects already-in-shell
state), sentinel-bracketed run() that
distinguishes shell output from the TTY
echo of input by requiring a leading
\r\n boundary on the marker.
tools/vm_load_controller.py in-guest load controller. set_phase()
dispatches the per-phase shell command
over the serial connection.
tools/run_real_vm_demo.py ties it all together: boot VM, wait for
cloud-init runcmd, log in, run the
EpisodeRunner with on_phase=controller,
shut down VM.
Deps: paramiko, pycdlib added.
docs/sources.md updated with Alpine cloud image (sha512 pinned), and
the new Python deps.
README leads with the tier-2 plot now (real VM, real workload). The
previous synthetic plot is moved below with explicit "host-side mimic,
not a VM" labelling. Tier-2 status flipped to ✅ in the tier table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
52 lines
1.8 KiB
Bash
Executable file
52 lines
1.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Boot the Cirros qcow2 under KVM with QMP and a monitor socket exposed.
|
|
#
|
|
# This is the v0 VM launcher for phase 2: validate that the orchestrator
|
|
# and host /proc collector work against a real qemu-system process. No
|
|
# host-only bridge yet, no exploit driver, no payload — just boot and
|
|
# idle. We add the bridge and exploit machinery in later phases.
|
|
#
|
|
# Run dir is exported so the orchestrator can read the qemu pid:
|
|
# $RUN_DIR/qemu.pid
|
|
# $RUN_DIR/qmp.sock
|
|
# $RUN_DIR/monitor.sock
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
IMAGE="${IMAGE:-$REPO_ROOT/vm/images/alpine-baseline.qcow2}"
|
|
CIDATA="${CIDATA:-$REPO_ROOT/vm/images/cidata.iso}"
|
|
RUN_DIR="${RUN_DIR:-/tmp/cis490-vm}"
|
|
|
|
mkdir -p "$RUN_DIR"
|
|
QMP_SOCK="$RUN_DIR/qmp.sock"
|
|
MON_SOCK="$RUN_DIR/monitor.sock"
|
|
PID_FILE="$RUN_DIR/qemu.pid"
|
|
|
|
if [[ ! -f "$IMAGE" ]]; then
|
|
echo "no image at $IMAGE" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$CIDATA" ]]; then
|
|
echo "no cidata at $CIDATA — build it with: uv run python tools/build_cidata.py $CIDATA" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# snapshot=on routes guest writes through a temporary overlay so the qcow2
|
|
# on disk is never mutated — every boot starts from the same bytes.
|
|
exec qemu-system-x86_64 \
|
|
-name cis490-vm \
|
|
-machine q35,accel=kvm \
|
|
-cpu host \
|
|
-smp 1,sockets=1,cores=1,threads=1 \
|
|
-m 256 \
|
|
-drive file="$IMAGE",format=qcow2,if=virtio,snapshot=on \
|
|
-drive file="$CIDATA",format=raw,if=virtio,readonly=on \
|
|
-netdev user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22 \
|
|
-device virtio-net-pci,netdev=n0 \
|
|
-nographic \
|
|
-serial unix:"$RUN_DIR/serial.sock",server=on,wait=off \
|
|
-monitor unix:"$MON_SOCK",server=on,wait=off \
|
|
-qmp unix:"$QMP_SOCK",server=on,wait=off \
|
|
-pidfile "$PID_FILE" \
|
|
-display none
|