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
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
codesignpass after the build:A bare
swift buildproduces a binary Virtualization.framework refuses. Onlygate_build_runnerand the 16 legacyverify-*.shcodesign lines apply it — andVGATE_NO_BUILD=1skips 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_supportreports 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.ccallshv_vm_create, is built and signed once into.build/hv-probe/(reused across a fleet sweep) withtools/gate/hv-probe.entitlements, and reports its code.Each code is attributed instead of collapsed into one "unavailable":
0x00000000HV_SUCCESS0xfae9400fHV_UNSUPPORTED (+ the other refusals)0xfae94007HV_DENIEDThe
HV_DENIEDrow is the trap this repository already hit once, now handled at runtime rather than in prose — and it is why a directhv_vm_createneedscom.apple.security.hypervisor, a different key from thecom.apple.security.virtualizationthe runner uses. When the probe cannot be produced at all (noclang, missing sources) the preflight says so and falls back tokern.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_vzintools/lib/gate-run.sh; called fromgate_build_runner(where the signature was just applied) and from thevgate.shpreamble ahead ofgate_begin, so nothing boots before it passes. Failures name the cause and the fix:A checker whose negative path is never exercised is the bug it was written to catch, so
tools/gate/test-gate-run.shdrives the whole code→verdict mapping withcodesign,sysctland 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 carriescom.apple.security.hypervisor. Plus source-level guards that the preflight is still wired, since deleting the call is the silent regression. Wired intojust verify-portableandci.yml; the parity lint covers the pair by construction (19 commands).Evidence
tools/gate/test-gate-run.shhv_vm_create -> 0x00000000 (HV_SUCCESS), probe verifiably signed for the hypervisor entitlementlive-sched-ringPASS 1/1vgateaborts rc=1 with 0 runs started (no VM boot)lint-workflows.shinventory-gates --checkTwo 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
sysctlused${VAR:-1}, coercing an explicitly empty value back to1; and the shipped-probe case initially ran with the codesign stub still onPATH, so the "real" probe was never signed and reportedHV_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.