shutil.rmtree's error callback was renamed onerror -> onexc in 3.12 and the
two receive different third arguments, so sandbox_destroy raised TypeError on
every 3.11 run despite requires-python = ">=3.11". Route both through one
helper and test it.
Found by running the suite under 3.11 before publishing; the whole suite now
passes on 3.11 and 3.13.
The unit tests mock QMP and the guest agent, so they cannot catch a wrong
argument name, a QEMU option that stopped parsing, or a reply shape that
differs from the fake. This suite drives the real thing: the qemu-img
toolchain, lifecycle and see-and-drive on a diskless BIOS VM, adopting a
forgotten VM through attach_vm, the refusal to reuse a live socket, and the
full sandbox journey (guest exec, file round trip, live snapshot create /
restore / delete verified by guest state, screenshot) with a check that the
base image is never modified.
It immediately earned its keep: pid_matches_vm was rejecting attached VMs,
because an externally launched QEMU carries whatever -name its launcher
chose, so adopting one under a different name made it read as stopped. The
identity check now applies only to VMs we spawned.
QEMU command line:
- escape commas in every interpolated path (qopt); a path like
'data,readonly=on.qcow2' previously injected a drive option
- reject extra_args flags that breach VM isolation (host filesystem
passthrough, host block devices, spawning chardevs, -runas) and document
the parameter as operator-only
- detect duplicate host ports across port_forwards instead of failing at
QEMU launch; auto ports no longer collide with each other
Sandbox isolation:
- sandbox_vm now blocks guest-initiated traffic by default (restrict=on),
with allow_network=True to opt in. Verified end to end: with identical
guest network state, a default sandbox reaches neither a host loopback
service nor the internet, while allow_network=True reaches both
- note in the docstring that the guest agent answers before the guest has
finished booting
Bounded work per call:
- vm_serial_read seeks a 256KB window from the end instead of reading a
console log that grows without bound into memory
- cap vm_type_text length and vm_mouse_move deltas
- screenshots get a unique filename and are cleaned up, so a concurrent
capture cannot swap the frame under vm_click
Identity and liveness:
- attach_vm requires an actual unix socket and stores the resolved path
- attached VMs are judged by connecting, not by a stat that a stale socket
file would pass
- refuse to act on a PID whose cmdline proves it is a different VM
- a sandbox's base image counts as in use while its overlay is live
- fix a latent NameError in vm_mouse_move's homing branch
The success paths were fine; several failure paths drew a confident
conclusion without checking the thing they waited for.
Registry (was: any parse error killed the whole server, since load() runs
in the lifespan):
- quarantine an unreadable file and start empty instead of raising, so the
tools that stop runaway VMs keep working when bookkeeping is damaged
- skip malformed or invalidly-named records rather than failing the load;
report both through list_vms as registry_warnings
- read-modify-write under an exclusive flock so a second instance merges
instead of clobbering, with a PID-unique temp file
- drop the lifespan shutdown save, which could resurrect deleted records
- version the schema and round-trip unknown record fields
sandbox_destroy (the only tool that deletes files):
- verify the process actually died, escalating to SIGKILL, and refuse to
delete an overlay QEMU still holds open
- assert the target is inside the VM state tree before rmtree
- report cleanup errors instead of swallowing them; destroyed now reflects
what happened
Launch races:
- reserve the name before the first await so two concurrent launches cannot
race over one set of sockets
- refuse to unlink a QMP socket that is still accepting connections
- register the VM with a warning rather than orphaning it when the pidfile
is unreadable but QEMU is up
Guest agent and QMP:
- bound every guest-agent call, not just the handshake; cap max_bytes and
stop guest_file_read spinning on a zero-progress agent
- serialize QMP sessions per VM (the monitor is single-client) and say
"another operation holds it" instead of "the VM has likely exited"
- poll liveness while waiting for SHUTDOWN so a crashed VM is reported as
exited rather than as a guest ignoring ACPI
- default command timeout, with a longer bound for savevm/loadvm
- stricter snapshot tags; log destructive operations to stderr
Adds tests/test_reliability.py covering the conditions above.