Skip to content

Ask QEMU whether it accepts the command line this package writes - #4

Merged
aledbf merged 1 commit into
mainfrom
machine/qemu-accepts-the-arguments
Sep 8, 2026
Merged

Ask QEMU whether it accepts the command line this package writes#4
aledbf merged 1 commit into
mainfrom
machine/qemu-accepts-the-arguments

Conversation

@aledbf

@aledbf aledbf commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The machine package writes a QEMU command line by hand and nothing checked that the pinned binary accepts it. The device set is decided in qemu/devices.mak; the device and property names are typed out in machine/machine.go. The two move independently, and a device dropped from the allowlist, a property renamed at a version bump, and a typo added here all surface the same way: a VM that does not start, days later, reading like a fault in whatever launched it.

What it asserts

task verify:args builds nine Specs — with and without a memory file, with and without a memory ceiling (which is what adds virtio-mem), with and without vsock, with disks, with a NIC, with a named CPU model, and one with all of it — starts each under the TCG binary with -S, and requires an answer on QMP. A process that replies to query-status has already created and realized every device on its command line, which is a stronger claim than "it parsed". Nine machines start, answer and are killed in 0.23 s together.

The rewrites, and what is left uncovered

CI has no /dev/kvm and the binary a tenant's host runs refuses to start without it, so only the TCG build can answer. Exactly three arguments are rewritten, each printed in the log:

  • accel=kvmaccel=tcg
  • -cpu hostmaxhost is KVM-only by construction; max is the other model that derives its features from the silicon, so migratable=on stays under test
  • a NIC's tap backend → a hub port

Not covered, and said so rather than hidden: the tap,id=netN,fd=N,vhost=on line itself (a TAP fd needs CAP_NET_ADMIN; the virtio-net-pci device that references it is checked verbatim, romfile= and slot included); a real named microarchitecture (enforce=on is exactly what refuses one under TCG — Skylake-Server-v4 gives "TCG doesn't support requested features", exit 1; qemu64 is used so enforce itself is checked); and vhost-vsock-pci on a host with no vhost_vsock module, which both workflows modprobe and which names itself as unchecked otherwise.

The kernel is a 197-byte ELF with a PVH entry note written by the test, so the check does not depend on the kernel build. QEMU still enters it through pvh.bin, so -L is exercised.

Proving it can fail

  • free-page-reporting misspelt → -device virtio-balloon-pci,free-page-reportng=on,...: Property 'virtio-balloon-pci.free-page-reportng' not found
  • virtio-mem-pci as virtio-mem-pcie'virtio-mem-pcie' is not a valid device model name

Both under the full command line, one argument per line.

Where it runs

ci.yml, on every push — which is where machine/*.go changes and where qemu.yml, filtered to qemu/**, never fires. It still builds nothing: task qemu:fetch unpacks the published runtime image for the pinned version into the same _output/ tree a build writes, in seconds, and ends in the same qemu:verify. Also in qemu.yml, against the freshly built binary — which covers the one case ci.yml cannot, a pull request bumping the pin to a version not yet published.

task test is unchanged: with no binary the test skips and says so. task verify:args refuses instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a

Nothing did. The device set is decided in qemu/devices.mak and the device and
property names are typed out by hand in machine/machine.go — virtio-balloon-pci
with free-page-reporting and deflate-on-oom, virtio-mem-pci with requested-size,
vhost-vsock-pci, vmgenid, virtio-net-pci with romfile=, the ICH9-LPC globals, the
four -sandbox restrictions — and the two halves move independently. The package's
tests assert on the strings it produces, which proves it agrees with itself. A
device dropped from the allowlist, a property renamed at the next version bump
and a typo added here therefore all produce the same thing: a VM that does not
start, found days later by whoever booted one, in an error that reads like a
fault in whatever launched it.

`task verify:args` builds nine Specs — with and without a memory file, with and
without a memory ceiling, with and without a vsock, with disks, with a NIC, with
a named CPU model, and one with all of it — and starts each under the TCG binary
with -S, then requires an answer on QMP. "It parsed" would have been the weaker
claim: a process that replies to query-status has already created and realized
every device on its command line. Nine machines start, answer and are killed in
0.23 s together, which is less than one boot.

Deliberately broken to check it goes red and says where: free-page-reporting
misspelt gives `-device virtio-balloon-pci,free-page-reportng=on,...: Property
'virtio-balloon-pci.free-page-reportng' not found`, and virtio-mem-pci as
virtio-mem-pcie gives `'virtio-mem-pcie' is not a valid device model name`, each
under the full command line, one argument per line.

**Three arguments are rewritten, and no others.** CI has no /dev/kvm and the
binary a tenant's host runs refuses to start without it, so the question can only
be put to the TCG build — and `accel=kvm` becomes `accel=tcg`, `-cpu host`
becomes `max` (a KVM-only value by construction: "CPU model 'host' requires KVM
or HVF"; max is the other model that derives its features from the silicon, so
migratable=on stays under test), and a NIC's tap backend becomes a hub port. Each
rewrite is printed, so how far the claim reaches is visible in the log rather
than buried here.

What that leaves uncovered, stated rather than hidden:

  - `tap,id=netN,fd=N,vhost=on`. The backend is a file descriptor the caller
    opened and a test cannot open one without CAP_NET_ADMIN. The device that
    references it is checked verbatim, romfile= and slot included, which is the
    half a release can get wrong.
  - A real named CPU model. enforce=on is precisely what stops one being checked
    under emulation — Skylake-Server-v4 gives ten warnings and then "TCG doesn't
    support requested features", exit 1, which is the flag doing its job. qemu64
    is used instead, so what is checked is the package's own contribution: that
    enforce is a property this binary has.
  - vhost-vsock-pci on a machine that will not give up /dev/vhost-vsock. Both
    workflows modprobe it *and chmod it*, which is not decoration: the first run
    of this on a GitHub runner loaded the module, found the node, and failed with
    "Could not open '/dev/vhost-vsock': Permission denied" — indistinguishable,
    in a stat, from having it. The test opens the device rather than stat'ing it
    for the same reason, and a host that refuses names the device as unchecked
    instead of passing.

The kernel is a 197-byte ELF with a PVH entry note, written by the test. Not the
real one, deliberately: what is under test is the command line, and requiring
vmlinux would tie this to the kernel build — tens of minutes — so it could only
run in a lane that had one, which is the lane a machine/*.go change never
reaches. QEMU still enters it through pvh.bin out of the firmware directory, so
-L is exercised either way. Without the note it says "Error loading uncompressed
kernel without PVH ELF Note", which is how the stub was checked.

It runs in ci.yml, on every push, which is where machine/*.go is changed and
where qemu.yml — filtered to qemu/** — never fires. ci.yml still builds nothing:
`task qemu:fetch` unpacks the published runtime image for the version
qemu/Dockerfile pins into the same _output/ tree a build writes, in seconds, and
ends in the same qemu:verify. The one case that cannot work is a pull request
bumping the pin, since publishing happens from main — and that pull request
changes qemu/**, so qemu.yml builds the binary and runs the identical check
against it, which is the other place this was added.

`task test` is unchanged: with no binary in _output/ the test skips and says so.
`task verify:args` refuses instead, because a gate that passes for want of a
binary is worse than no gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
@aledbf
aledbf force-pushed the machine/qemu-accepts-the-arguments branch from 35f43ac to 1d5f975 Compare September 8, 2026 04:15
@aledbf
aledbf merged commit fe332b4 into main Sep 8, 2026
2 checks passed
@aledbf
aledbf deleted the machine/qemu-accepts-the-arguments branch September 8, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant