Skip to content

VZ gate preflight: a runner without its entitlements, or a host that cannot host a guest, must fail loudly instead of looking like a broken gate (#1259) - #1260

Merged
drawmeanelephant merged 2 commits into
mainfrom
agent/buffy/vz-preflight
Sep 14, 2026
Merged

drawmeanelephant merged 2 commits into
mainfrom
agent/buffy/vz-preflight

Conversation

@drawmeanelephant

@drawmeanelephant drawmeanelephant commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Closes #1259

A class-B gate that fails because the harness is wrong looks exactly like a class-B gate that fails because the code under test is wrong. Two ways that happens, both now named in advance instead of diagnosed afterwards.

1. The runner binary has no entitlements

The entitlement comes from an ad-hoc codesign pass after the build:

swift build --package-path host/vm-runner --configuration release
codesign --force --sign - --entitlements host/vm-runner/entitlements.plist \
    host/vm-runner/.build/release/VMRunner

A bare swift build produces a binary Virtualization.framework refuses. Only gate_build_runner and the 16 legacy verify-*.sh codesign lines apply it — and VGATE_NO_BUILD=1 skips the build entirely, so a stale or hand-built runner can reach a VM with nobody having checked it. That is the reachable path, and it is why the preflight runs for both branches of the preamble rather than only after a build.

2. The host cannot host a guest — asked directly

kern.hv_support reports what the kernel supports; it does not ask whether a process can obtain a hypervisor, which is what the gates depend on. So the verdict is now the call Virtualization.framework is itself built on, not a proxy: tools/gate/hv-probe.c calls hv_vm_create, is built and signed once into .build/hv-probe/ (reused across a fleet sweep) with tools/gate/hv-probe.entitlements, and reports its code.

gate-run: host 27.0/arm64; kern.hv_support=1 hv_vmm_present=0; hv_vm_create -> 0x00000000 (HV_SUCCESS)

Each code is attributed instead of collapsed into one "unavailable":

code verdict
0x00000000 HV_SUCCESS passes
0xfae9400f HV_UNSUPPORTED (+ the other refusals) fails as a host verdict
0xfae94007 HV_DENIED fails as a harness fault — a statement about the signature wearing the shape of a statement about the machine
anything unrecognised fails; refusing to guess is why we asked directly

The HV_DENIED row is the trap this repository already hit once, now handled at runtime rather than in prose — and it is why a direct hv_vm_create needs com.apple.security.hypervisor, a different key from the com.apple.security.virtualization the runner uses. When the probe cannot be produced at all (no clang, missing sources) the preflight says so and falls back to kern.hv_support, so a missing compiler degrades the check rather than silently passing it.

What lands

gate_assert_runner_entitled, gate_report_hv_capability, gate_preflight_vz in tools/lib/gate-run.sh; called from gate_build_runner (where the signature was just applied) and from the vgate.sh preamble ahead of gate_begin, so nothing boots before it passes. Failures name the cause and the fix:

gate-run: ERROR — host/vm-runner/.build/release/VMRunner is NOT entitled with com.apple.security.virtualization.
  A bare 'swift build' produces a binary Virtualization.framework
  will refuse; the entitlement comes from an ad-hoc codesign pass
  AFTER the build. ...
      codesign --force --sign - \
          --entitlements host/vm-runner/entitlements.plist host/vm-runner/.build/release/VMRunner
  or rebuild through gate_build_runner. (codesign said: ...: code object is not signed at all)

A checker whose negative path is never exercised is the bug it was written to catch, so tools/gate/test-gate-run.sh drives the whole code→verdict mapping with codesign, sysctl and the probe stubbed — class A, no VM, no Apple silicon — and builds and signs the shipped probe for real, asserting it compiles, reports a recognised code, and carries com.apple.security.hypervisor. Plus source-level guards that the preflight is still wired, since deleting the call is the silent regression. Wired into just verify-portable and ci.yml; the parity lint covers the pair by construction (19 commands).

Evidence

check result
tools/gate/test-gate-run.sh 35/35 PASS
real preflight on this host hv_vm_create -> 0x00000000 (HV_SUCCESS), probe verifiably signed for the hypervisor entitlement
live gate through the new preflight live-sched-ring PASS 1/1
real negative signature stripped from the built runner → vgate aborts rc=1 with 0 runs started (no VM boot)
lint-workflows.sh class-A parity OK — 19/19 commands
inventory-gates --check OK (fleet inventory re-rendered)
coordination gate ok — 3 open claims, no overlaps

Two bugs the new test caught in itself, both worth the note: the first version of the mapping test passed a case vacuously because the stubbed sysctl used ${VAR:-1}, coercing an explicitly empty value back to 1; and the shipped-probe case initially ran with the codesign stub still on PATH, so the "real" probe was never signed and reported HV_DENIED — the stub catching itself. The code was right in both cases; the tests were not.

Class-B runs need VZ hardware (just verify-vz), so the live rows are local runs on this host, as the project's evidence rules require. CI proves the portable set.

…se (#1259)

Class-B gates fail in two ways that both show up at VM boot wearing the
wrong label.

A runner built with a bare `swift build` carries no entitlements, so
Virtualization.framework refuses it and the error reads like a hardware
or configuration problem rather than a missing signature. Only
gate_build_runner (tools/lib/gate-run.sh) and the 16 legacy verify-*.sh
codesign lines apply it, and VGATE_NO_BUILD=1 skips the build entirely, so
a stale binary could reach a VM without anyone having checked it.

A host without Hypervisor.framework cannot boot a guest at all, and the
gate under test takes the blame. Measured 2026-09-14 on GitHub's hosted
runners: kern.hv_support=0 with hv_vmm_present=1 (the runner is itself a
guest) and hv_vm_create returns 0xfae9400f HV_UNSUPPORTED. Note the code
matters -- a mis-signed probe returns 0xfae94007 HV_DENIED instead, a
verdict about the signature standing where a verdict about the machine is
expected, which is why the entitlement is asserted separately.

Adds gate_assert_runner_entitled, gate_report_hv_capability and
gate_preflight_vz, runs the preflight from gate_build_runner (where the
signature was just applied) and from the vgate preamble (for BOTH branches,
so VGATE_NO_BUILD=1 is covered), and fails loudly with the fix named.

test-gate-run.sh exercises both failure modes with `codesign` and `sysctl`
stubbed, so it proves the negative paths on a CI runner with no VM and no
Apple silicon -- a checker whose negative path is never run is the bug it
was written to catch. Wired into `just verify-portable` and ci.yml; the
parity lint covers the pair.

Evidence: test 22/22; verify-portable parity 19/19 commands; real (unstubbed)
positive on this host prints "host 27.0/arm64; kern.hv_support=1
hv_vmm_present=0" and passes; real negative -- signature stripped from the
built runner -- aborts vgate with rc=1 before any run starts; re-signed, the
same spec boots and passes 1/1.
…support (#1259)

kern.hv_support reports what the KERNEL supports; it does not ask whether a
process can obtain a hypervisor, which is what the gates actually depend on.
Replace the proxy with the call Virtualization.framework is itself built on.

tools/gate/hv-probe.c compiles to a tiny program that calls hv_vm_create and
prints its code and name; gate_build_hv_probe builds and ad-hoc signs it once
into .build/hv-probe/ (rebuilt only when the source is newer, so a fleet sweep
pays for it once) with tools/gate/hv-probe.entitlements, which grants
com.apple.security.hypervisor.

The verdict now carries the code:

  gate-run: host 27.0/arm64; kern.hv_support=1 hv_vmm_present=0; hv_vm_create -> 0x00000000 (HV_SUCCESS)

and each code is attributed correctly rather than collapsed into one
"unavailable":

  * HV_SUCCESS passes.
  * HV_UNSUPPORTED (and the other refusal codes) fails as a HOST verdict.
  * HV_DENIED fails as a HARNESS fault, and says so: a mis-signed probe
    returns 0xfae94007, which is a statement about the signature wearing the
    shape of a statement about the machine. That is the trap this repository
    already hit once, now handled at runtime instead of in prose.
  * an unrecognised code fails rather than falling through to the weaker
    sysctl check -- refusing to guess is the point of asking directly.

When the probe cannot be produced at all (no clang, missing sources) the
preflight says so and falls back to kern.hv_support, so a missing compiler
degrades the check instead of silently passing it.

The class-A test drives the whole mapping through a stub probe and a stub
sysctl, covers the fallback path and the unrecognised code, and now also
builds and signs the SHIPPED probe for real -- asserting it compiles, reports
a recognised code, and carries com.apple.security.hypervisor. That last case
is a real check of the new entitlement file, and it caught the test's own
harness first: with the codesign stub still on PATH, the "real" probe was
never signed and reported HV_DENIED.

Evidence: test 35/35 (up from 22); real preflight on this host reports
hv_vm_create -> 0x00000000 (HV_SUCCESS) with the probe verifiably signed for
the hypervisor entitlement; live-sched-ring boots through the new preflight
and passes 1/1; inventory --check OK; parity 19/19; coordination ok.
@drawmeanelephant
drawmeanelephant merged commit 81a4f38 into main Sep 14, 2026
9 checks passed
@drawmeanelephant
drawmeanelephant deleted the agent/buffy/vz-preflight branch September 14, 2026 17:05
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.

claim: VZ gate preflight: a runner binary without its entitlements must fail loudly, and the host's hypervisor capability must be stated

1 participant