# Why each VMAware check fires, and what it took to clear it The engineering record behind the fidelity work. `README.md` says *what to apply and when*; this says *why*, with the source references. Written 2026-09-03 at 2/85, revised 2026-09-05 at **1/85** on both VMAware v2.8.1 and VMAware HEAD (commit 95fecc2) - the one survivor is `GPU_CAPABILITIES`, and VMAware's own verdict is "Running on bare metal", likeliness 20%, confirmation false. Source read against: VMAware `src/vmaware.hpp`, QEMU 11.1.2, kernel 7.2.3 `arch/x86/kvm`. --- ## CLEARED - CPUID_SIGNATURE **Not about hypervisor leaves.** The guest's `0x40000000` reads all zeros, and so does the bare-metal host, so it was never the trigger. The AMD branch (`cpuid_signature()`, vmaware.hpp:6941-6970) reads **leaf 7 subleaf 0, EDX** and returns true if bit 26 (`IBRS`/`IBPB`), 27 (`STIBP`) or 31 (`SSBD`) is set. AMD reserves those three to zero and enumerates its mitigations in `0x80000008.EBX` instead. Measured with an unprivileged CPUID probe: | | leaf 7.0 EDX | bits 26/27/31 | `0x80000008.EBX` 12/14/15/24/25 | | --- | --- | --- | --- | | host, bare metal | `0x10000010` | `0 0 0` | `1 1 1 1 1` | | guest, before | `0x9c000010` | set | passed through | | guest, after | `0x30000010` | `0 0 0` | `1 1 1 1 1` | KVM synthesises the Intel-style `SPEC_CTRL` interface on AMD, and `host-passthrough` enables it. Three `` lines fix it. All three are needed - the check ORs the bits. Verified in-guest afterwards that every AMD-native mitigation bit survives, so the guest keeps the interface Windows actually uses on AMD. Residual: KVM also synthesises bit 29 (`arch-capabilities`), which bare metal lacks. Nothing reads it, so it is left alone. --- ## CLEARED - SVM_EXCEPTIONS **A bug KVM documents against itself.** `svm.c:272` is unconditional: ```c svm->vmcb->save.efer = efer | EFER_SVME; ``` Hardware therefore never sees the guest's real `SVME=0`. A CPL3 `VMLOAD` passes the SVME check, hardware notices CPL is not zero, and injects `#GP` *before* the VMLOAD intercept can fire. Bare metal raises `#UD`, and `#UD` is the one exception `svm_exceptions()` treats as innocent (vmaware.hpp:14551). There is a `FIXME` at `svm.c:1076-1080` describing exactly this. The diagnostic that pinned it: the scored run printed `[ DETECTED ]` with **no debug line**. Both noisy exits in the function call `vma_debug`, so it must have fallen through to the bare `return true` at vmaware.hpp:14570 - which forces `svm_visible == true` and a non-`#UD` fault. Fix, in `svm_recalc_instruction_intercepts()`: ```c if (!(vcpu->arch.efer & EFER_SVME)) { svm_set_intercept(svm, INTERCEPT_VMLOAD); svm_set_intercept(svm, INTERCEPT_VMSAVE); svm_set_intercept(svm, INTERCEPT_CLGI); svm_set_intercept(svm, INTERCEPT_STGI); set_exception_intercept(svm, GP_VECTOR); /* added */ } else { ``` `gp_interception()` then decodes the SVM opcode, sees `!is_guest_mode`, and calls `svm_invoke_exit_handler(SVM_EXIT_VMLOAD)` -> `vmload_interception` -> `nested_svm_check_permissions` (nested.c:1664), which queues `UD_VECTOR`. The shipped `0001` also keeps the intercept across a guest clearing `SVME` later (the stock `svm_set_efer()` dropped it there), drops it again on `SVME=1` where the erratum workaround does not need it, and never arms it for SEV guests, whose instructions KVM cannot decode - the same exclusion the erratum path already makes. **Do NOT disable `svm` in guest CPUID as an alternative.** It does not change the exception, and it moves the check into its `!svm_visible` branch, which is *worse* - "SVM hypervisor hiding CPU capabilities", weight 150. **Do NOT use `kvm.enable_vmware_backdoor=1`** as the no-rebuild shortcut. It arms the same intercept, but permanently opens two well-known VMware detection surfaces in every guest on the host: `emulate.c:2562` stops faulting CPL3 `IN`/`OUT` on ports `0x5658`/`0x5659`, and `emulate.c:3905` makes CPL3 `RDPMC` of pseudo-counters `0x10000`-`0x10002` return host TSC, which is architecturally impossible on real silicon. VMAware is blind to both. pafish, al-khaser and other detection tooling are not. --- ## CLEARED - FIRMWARE **A chain that reports only its first hit.** `firmware()` (vmaware.hpp:8860) runs six sections in fixed order against each firmware buffer, DSDT first, and the first match returns. Fixing one link costs a full QEMU rebuild and boot and only reveals the next - so bundle. Links, in order: 1. ~~SMI Resources reservation string~~ - `_UID` renamed 2. ~~PRTP/PRTA routing symmetry~~ - renamed `IRQP`/`IRQA` 3. ~~Sequential PIRQ names, vmaware.hpp:9142~~ - `GSIA-H` -> `APCA-H` 4. ~~PNP0A06 resource stubs, vmaware.hpp:9148~~ - `GPER` -> `RSRA`, `PHPR` -> `RSRB` 5. ~~FACP C2/C3 latencies, vmaware.hpp:9242~~ - `0xfff` -> `0xffe` 6. Debug Port OperationRegion at `0x0402` - already dead on this tree 7. HPET register-validation loop - dormant, domain sets `hpet present='no'`. Re-arms if enabled. 8. Dummy SATA, DMAR, APIC source overrides - already dead on this tree Link 3 requires **all four** of `LNKE`, `LNKH`, `GSIE`, `GSIH`, which is QEMU's eight-link plus eight-GSI layout. Real boards have four PCI link devices and no GSI-named ones, so rename the `GSI*` half and leave `LNK*` alone. Those names are declared by `build_gsi_link_dev()` and referenced only through `build_q35_routing_table("GSI")`, which derives every name from one 3-character prefix - so a prefix change moves every reference with it. Do not pick `IRQ` as the replacement: `_SB.PCI0.IRQA` is already the APIC routing package and a bare NameSeg would bind to it. `0xffe` keeps "C-state not supported" semantics (anything above 100 / 1000 means unsupported) while not matching the check's exact-equality test against `0x0FFF`. **WAET must stay out.** `"WAET"` is target index 18 in the section-2 scan and the table's signature sits at offset 0, i.e. an instant hit. An earlier sed-based patcher claimed it restored WAET and never did: `build_waet()` has zero call sites, so its `grep -q build_waet` guard was satisfied by the definition alone and the insert never ran. Guard on a call site, not on a definition. **The chain grew.** `firmware()` returns on its first hit, so clearing one link only exposes the next, and current VMAware has added links past the eight above. `09-firmware-chain` clears the rest, measured to 0 detections on both v2.8.1 and HEAD: | Link | Fix | | --- | --- | | IQST/IQCR + PRR0/PRRI interrupt-query methods | renamed PSTA/PCRS/PRS0/PRSI | | TPM PPI regions/method TPP2/TPP3/TPFN | renamed PPI2/PPI3/PPIF | | zeroed `_S5` SLP_TYP package | 2-element package form | | `ACPI0006` `_HID` in `\_GPE` | declared by full path `\_GPE._HID`; dropping it stops macOS powering off | | Q35 MMCONFIG reservation device `MCFG` | renamed `PCMM` | | synthetic GSI link devices, empty `_DIS`/`_SRS` | gave the no-op methods a body | | PCIe `_OSC` `And(CDW3, 0x1F, ..)` mask | swapped the `And()` operands (commutative) | | PIRQ link `_PRS` fixed IRQ 5/10/11 | listed the same set as `{11, 10, 5}` | A q35 guest also needs a named USB controller in the DSDT (the ICH9 EHCI/UHCI set, declared `EHC1`/ `UHC1`), or the release flags "complete absence of USB controller declarations"; that is a domain choice, not a QEMU patch. ### CLEARED - the CPU-hotplug I/O signature that macOS forced back in (`11-macos-cpu-hotplug`) macOS references `\_SB.CPUS.Cxxx` from its own CpuPlug SSDT, so the guest must carry QEMU's `build_cpus_aml` output (the `\_SB.PCI0.PRES` controller plus the `\_SB.CPUS` `Processor` objects) or it hangs in ACPI namespace init. `01-firmware` had dropped that whole block, which is why macOS lived in an opt-in patch: the block reintroduces exactly one FIRMWARE tell, so a macOS-capable build scored 2/85 on v2.8.1 while HEAD stayed clean. The tell is a single check. v2.8.1 `firmware()` carries `cpu_hotplug_io[] = {0x0B, 0xD8, 0x0C}` and byte-matches it anywhere in the tables. Those three bytes are `WordData 0x0CD8` - the CPU-hotplug register port that `build_cpus_aml` writes into the `PRST` `OperationRegion` base (`ICH9_CPU_HOTPLUG_IO_BASE`). Confirmed by dumping the macOS-build DSDT: the bytes appear only at the `OperationRegion` operand, never in the `_CRS`. A small-resource `IO` descriptor encodes `0x0CD8` as `0x47 0x01 0xD8 0x0C 0xD8 0x0C 0x01 0x0C` - the `0xD8 0x0C` runs are preceded by `0x01`/`0x0C`, never `0x0B` - so the `_CRS` is not what matches and does not need touching. The renamed `_UID` ("Reserved resources C") already dodges the separate `"CPU Hotplug resources"` string check; it was never the live tell. HEAD has no `cpu_hotplug_io` check, so it never flagged the block. **Relocating the base does not work.** The obvious fix - move `ICH9_CPU_HOTPLUG_IO_BASE` off `0x0CD8` so the pattern changes - clears the scanner but hangs every SMM/OVMF guest: macOS spins ~10 cores at boot and never reaches login, Windows the same. `OvmfPkg/Include/IndustryStandard/Q35MchIch9.h` hardcodes `ICH9_CPU_HOTPLUG_BASE 0x0CD8` and OVMF's SMM code reads the register block at that fixed port, independent of the AML. The non-SMM path (plain OVMF, a bare Linux boot) survives a move; the SMM guests we actually run do not. A/B confirmed it: macOS reaches login on `0x0CD8`, spins on `0x0CE4`. Patching OVMF is already on the rejected list. **The fix keeps the port and hides only the AML operand.** `11` emits the `OperationRegion` base for a SystemIO region as `Add(0x0C00, 0xD8)` instead of the literal `0x0CD8`. It evaluates to the identical `0x0CD8` at namespace load - the port QEMU decodes, the port in `_CRS`, and the port OVMF reads are all unchanged - but the AML is `0x72 0x0B 0x00 0x0C 0x0A 0xD8 ..`, so the `0x0B 0xD8 0x0C` run never appears. macOS's interpreter evaluates the computed base without complaint. Verified on QEMU 11.1.2 with this fix in the default series: the DSDT keeps `\_SB.CPUS`/`PRES`/ `Processor(C000..)` and `0x0CD8`, with no `0x0B 0xD8 0x0C` anywhere (iasl-clean); macOS boots to login and powers off via S5 in ~12 s; VMAware v2.8.1 and HEAD both report `detected=0` / `FIRMWARE=0` on the stealth guest; Windows (secboot + SMM) and Linux boot unchanged. The opt-in split is gone. --- ## CLEARED - CTYPE and SMBIOS_VM_BIT (the SMBIOS QEMU synthesises) On Linux, VMAware reads SMBIOS four ways: `DMIDECODE` and `DMI_SCAN` match VM vendor strings in `dmidecode -t system` and seven `/sys/class/dmi/id` files, `SMBIOS_VM_BIT` tests bit 4 of type 0 byte 19, and `CTYPE` fires on chassis type 1, Other (HEAD e1a9786, vmaware.hpp:8272, :8948, :9023, :8234). On Windows, `FIRMWARE` also scans the raw tables (`RSMB`, :10369) for `pc-q35` and `Q35 +` (:9726). The strings are domain identity. The bit, the chassis type and the default strings are what QEMU emits by itself. QEMU synthesises every required structure the domain does not supply. Switching that off (`smbios_defaults = false`) leaves only what the domain passes: either a complete real dump through `-smbios file=`, or a table with no processor, memory or end-of-table structures. `10-smbios` keeps it on and fixes what the synthesised structures leak: | Leak | Fix | | --- | --- | | type 3 chassis type hard-coded to 1 (Other) | defaults to 3 (Desktop), set with `-smbios type=3,chassis-type=N` | | type 0 VM bit set whenever type 0 fields are given | `vm` defaults off; libvirt `` cannot pass `vm=off` | | unset product and version strings `Standard PC (Q35 + ICH9, 2009)`, `pc-q35-` | `Unknown`, like the manufacturer from `04-fw-cfg` | Measured on a Linux q35 guest, the same score on VMAware v2.8.1 and HEAD: | SMBIOS from the domain | QEMU | Score | | --- | --- | --- | | complete real dump, 52 `file=` structures | defaults off | 0/85 | | the same dump | defaults on | 0/85, `dmidecode` byte-identical | | field arguments for types 0-4 with `vm=off` | defaults on, builders unpatched | 1/85, `CTYPE` | | field arguments for types 0-4, no `vm=off`, `chassis-type` set | `10-smbios` | 0/85 | Identity is authored as field arguments. A dump through `-smbios file=` adds what field arguments cannot express (memory module detail, OEM structures, battery) and mixes with them per type: a type given as `file=` cannot also take field arguments. Windows and macOS guests are not yet measured against this patch. --- ## OPEN - GPU_CAPABILITIES (weight 20) **Not a GPU check.** Four lines: `GetDC(nullptr)`, then `GetDeviceCaps(hdc, COLORMGMTCAPS)`, DETECTED if the result lacks `CM_GAMMA_RAMP` (0x2) or is `CM_NONE`. No adapter enumeration, no DXGI, no WMI, no device blacklist, no EDID - so the patched `SAM`/`SyncMaster` EDID override is irrelevant to this check. A plausible theory said it was a harness artefact: scoring runs over SSH, OpenSSH on Windows lands in session 0, and a non-interactive window station has no gamma LUT. **Tested and disproven.** Re-ran as a scheduled task with `/it`, confirmed `SessionId=1` and `ScreenBounds={0,0,640,480}` - same score, still fires. So it is genuine: `