| Project | Custom |
| License | N/A |
| Tracked | Custom update script |
vfio-stealth-nix is a NixOS module that makes VFIO/KVM virtual machines indistinguishable from bare-metal hardware. It provides hardware-accurate VM configuration -- realistic hardware identity instead of hypervisor artifacts.
For long-form references beyond the quick start below, see:
docs/ARCHITECTURE.md— directory layout, component-to-file mapping, kernel-integration boundarydocs/BUILD.md— operator commands: dev shell, formatters, hooks, tests, update contract, troubleshootingdocs/OPTIONS.md— canonicalvirtualisation.vfio-stealth.*option reference
| Package | Description |
|---|---|
| qemu-stealth | Patched QEMU with AutoVirt hardware-emulation patches + configurable hardware identifiers (EDID, ACPI OEM, disk/optical models, SCSI vendor, disk serial customization, fw_cfg DMA signature) |
| ovmf-stealth | Patched EDK2/OVMF firmware: clears VirtualMachine bit in SMBIOS Type 0, replaces Red Hat PCI vendor IDs, overrides ACPI OEM fields, strips BGRT boot logo (VMAware indicator). Overridable: secureBoot, msVarsTemplate, tpmSupport |
| acpi-ssdt-stealth | Compiled ACPI SSDT tables providing emulated embedded controller, fan, thermal zone, battery, power/sleep buttons, timers |
| smbios-stealth-tables | Binary SMBIOS tables for types QEMU cannot build via CLI (Type 7 cache, Types 26-29 probes) |
| smbios-extract | Host SMBIOS dump + anonymization tool for extracting real hardware strings to inject into VM config |
| stealth-detect | aarch64-linux only. Reports, per vector, whether an ARM guest can tell it is virtualized (device tree, PSCI conduit, MIDR, SMBIOS, ACPI OEM and FADT hypervisor identity, virtio, PCI, timer). Exits 1 when any vector fires |
| Vector | Technique | Where |
|---|---|---|
| CPUID hypervisor bit | Cleared via kvm.hidden + Hyper-V vendor_id override |
lib.nix (libvirt features) |
| CPUID leaf 0 vendor string | Hypervisor-Phantom: intercept at SVM level, override to AuthenticAMD, re-enter guest without full exit | kernel/cpuid-patch.nix |
| CPUID interception timing | Disabled entirely via cpuidPassthrough — guest CPUID runs at native speed | kernel/cpuid-disable.nix |
| RDTSC/RDTSCP timing | BetterTiming: track cumulative VM-exit time, subtract from TSC reads (RDTSC + RDTSCP handlers with compensated values) | kernel/timing-patch.nix |
| MSR_IA32_TSC reads | Compensated TSC value returned via patched kvm_get_msr_common |
kernel/timing-patch.nix |
| IA32_APERF/MPERF MSR | Passthrough via -cpu host,aperfmperf=on (covers IET-based detection) |
lib.nix (QEMU args) |
| SMBIOS Type 0 (BIOS) | Vendor, version, date, release override | module.nix options, lib.nix sysinfo |
| SMBIOS Type 1 (System) | Manufacturer, product, serial, family, UUID | module.nix options, lib.nix sysinfo |
| SMBIOS Type 2 (Baseboard) | Manufacturer, product, version, serial, asset tag, location | lib.nix sysinfo |
| SMBIOS Type 3 (Chassis) | Manufacturer, version, serial, asset tag | lib.nix QEMU args |
| SMBIOS Type 4 (Processor) | Socket, manufacturer, version, speed | lib.nix QEMU args (via cpuIdentity) |
| SMBIOS Type 7 (Cache) | L1/L2/L3 cache designation and sizes | lib.nix QEMU args |
| SMBIOS Type 8 (Port Connector) | USB port descriptors | lib.nix QEMU args |
| SMBIOS Type 9 (System Slots) | PCIe slot designation and type | lib.nix QEMU args |
| SMBIOS Type 17 (Memory) | DIMM manufacturer, part number, speed, size, count | module.nix options, lib.nix QEMU args |
| SMBIOS Type 26 (Voltage Probe) | Voltage probe description + min/max | lib.nix QEMU args |
| SMBIOS Type 27 (Cooling Device) | Cooling device type + speed | lib.nix QEMU args |
| SMBIOS Type 28 (Temperature Probe) | Temperature probe description + min/max | lib.nix QEMU args |
| SMBIOS Type 29 (Current Probe) | Current probe description + min/max | lib.nix QEMU args |
| ACPI table OEM IDs | Replaced ALASKA/AMI defaults with configurable strings (6-char + 8-char) | qemu/package.nix postPatch |
| ACPI SSDT devices | Emulated EC, fan, thermal zone, power/sleep buttons, timers | acpi/spoofed-devices.dsl |
| ACPI emulated battery | Control Method Battery (PNP0C0A) with BIF/BST methods | acpi/fake-battery.dsl |
| EDID display identity | Monitor manufacturer, model, serial, product code, DPI, manufacture date | qemu/package.nix arguments |
| Disk model string | IDE/SCSI disk model override in QEMU source | qemu/package.nix postPatch |
| Optical drive model | IDE/ATAPI optical drive model override | qemu/package.nix postPatch |
| MAC address OUI | Configurable OUI prefix for guest NIC | module.nix options |
| Hyper-V enlightenments | Full enlightenment set (relaxed, vapic, spinlocks, stimer, frequencies, etc.) with vendor_id override | lib.nix features |
| KVM feature hiding | kvm.hidden, hint-dedicated, poll-control |
lib.nix features |
| VMPort | Disabled | lib.nix features |
| Clock/timer emulation | kvmclock disabled, hypervclock enabled (enlightened mode) or disabled (hidden mode), native TSC, HPET present | lib.nix clock config |
| OVMF VirtualMachine bit | Cleared in SMBIOS Type 0 via EDK2 patch | ovmf/package.nix |
| OVMF PCI vendor IDs | Red Hat IDs replaced with AMD/Intel | ovmf/package.nix |
| VirtIO device identifiers | Balloon, RNG, tablet devices stripped from VM config | lib.nix devicesToRemove |
| SMBIOS Type 11 (OEM Strings) | Populated with realistic entries (empty Type 11 is a VM indicator) | module.nix options, lib.nix QEMU args |
| SMBIOS Type 41 (Onboard Devices) | Ethernet + SATA controller entries (prevents empty Win32_OnBoardDevice) | module.nix options, lib.nix QEMU args |
| Disk serial string | IDE drive serial set to realistic WD format instead of AutoVirt blank | qemu/package.nix postPatch |
| fw_cfg DMA signature + ACPI device | 8-byte DMA signature changed from "QEMU CFG" to "QCOM CFG"; fw_cfg ACPI device node removed from DSDT (4-byte probe at selector 0x0000 still reads "QEMU") | AutoVirt patch via qemu/package.nix |
| KVM paravirt MSR enforcement | kvm-pv-enforce-cpuid=on ensures guest_pv_has() rejects pvclock/steal-time MSRs when kvm.hidden=on |
lib.nix QEMU args |
| KVM hypercall patching | Disabled: emulator_fix_hypercall always injects #UD (bare-metal behavior on VMCALL/VMMCALL) | kernel/timing-patch.nix |
| SVM instruction interception | kvm_amd.vls=0 + kvm_amd.vgif=0 force VMLOAD/VMSAVE/STGI/CLGI interception |
module.nix kernel params |
| OVMF boot logo / BGRT | TianoCore LogoDxe + BootGraphicsResourceTableDxe stripped (VMAware CRC indicator) | ovmf/package.nix |
| ACPI thermal zone fluctuation | Timer()-based dynamic temperature in CPU + VRM thermal zones (handles static-value detection) | acpi/sensor-probes.dsl |
| PCI subsystem vendor:device | Default 0x1af4:0x1100 (Red Hat/QEMU) replaced with 0x8086:0x0000 (Intel) on all Q35 chipset devices | qemu/post-patch.nix |
| QEMU pvpanic device | Consumer excludes <panic> from domain XML; verified by verify-host.sh |
guest/verify-host.sh |
| Registry SCSI DEVICEMAP | Cleaned via guest-side registry script | guest/cleanup-registry.ps1 |
Add as a flake input:
vfio-stealth = {
url = "github:Daaboulex/vfio-stealth-nix";
inputs.nixpkgs.follows = "nixpkgs";
};Import the overlay and NixOS module:
imports = [ inputs.vfio-stealth.nixosModules.default ];
nixpkgs.overlays = [ inputs.vfio-stealth.overlays.default ];overlays.default provides qemu-stealth / ovmf-stealth (AMD) and
qemu-stealth-intel / ovmf-stealth-intel. Both vendors are built and
contract-tested; cpuVendor selects the AutoVirt patch family and the ACPI
OEM identity.
One limitation on Intel: the kernel-level stealth (timing.enable,
cpuidSpoof.enable, cpuidPassthrough.enable) is AMD/SVM-only -- those
patches target arch/x86/kvm/svm/svm.c, which kvm-intel never executes.
An assertion refuses that combination rather than shipping a feature that
silently does nothing. QEMU and OVMF stealth apply on both.
The stealth stack itself is x86_64-linux only, pinned there by each package's
meta.platforms: the AutoVirt QEMU patches target hw/i386/ and the Q35
southbridge, the EDK2 patches target OvmfPkg/OvmfPkgX64.{dsc,fdf} (on aarch64
pkgs.OVMF is ArmVirtQemu), and kernel/ edits arch/x86/kvm/svm/svm.c.
aarch64-linux currently carries only stealth-detect, the oracle that an
ARM spoof would have to defeat; see
docs/ARCHITECTURE.md for why the detector comes first.
Enable stealth with your own hardware strings:
virtualisation.vfio-stealth = {
enable = true;
cpuVendor = "amd"; # or "intel" -- required, no default
smbios = {
manufacturer = "Your Motherboard Manufacturer";
product = "Your Motherboard Model";
};
};All options live under virtualisation.vfio-stealth.*. Module options, build-time qemu-stealth arguments, and read-only outputs are documented in docs/OPTIONS.md.
WARNING: Do not use these exact values. Detection software can identify known stealth configurations. Use your own realistic hardware strings from
dmidecode,edid-decode, or manufacturer websites.
virtualisation.vfio-stealth = {
enable = true;
cpuVendor = "amd"; # or "intel" -- required, no default
smbios = {
manufacturer = "Micro-Star International Co., Ltd.";
product = "MAG X670E TOMAHAWK WIFI";
biosVendor = "American Megatrends International, LLC.";
biosVersion = "7E12vAH";
biosDate = "02/15/2025";
biosRelease = "2.3";
baseBoardVersion = "Rev 1.02";
baseBoardSerial = "K716234029";
serial = "K716234029";
socketPrefix = "AM5";
cache = {
l1 = 512;
l2 = 16384;
l3 = 65536;
};
oemStrings = [
"Default string"
"Default string"
"TOMAHAWK"
"Default string"
];
memory = {
manufacturer = "Corsair";
partNumber = "CMK32GX5M2B5600C36";
speed = 5600;
count = 2;
};
};
macPrefix = "2c:f0:5d"; # Peplink OUI
};
# QEMU overlay with matching hardware strings
nixpkgs.overlays = [
(final: prev: {
qemu-stealth = prev.qemu-stealth.override {
edidManufacturer = "BNQ";
edidSerial = "EX2780Q";
edidProductCode = "0x8532";
edidDpi = 109;
edidWeek = 24;
edidYear = 2022;
acpiOemId = "MSI_NB";
acpiOemTableId = "MEGABOOK";
diskModel = "WDC WD10EZEX-00WN4A0 ";
diskSerial = "WD-WMC4T0D2XYZA";
opticalModel = "HL-DT-ST DVDRAM GH24NSC0";
};
})
];virtualisation.vfio-stealth = {
enable = true;
cpuVendor = "amd"; # or "intel" -- required, no default
smbios = {
manufacturer = "Gigabyte Technology Co., Ltd.";
product = "B650 AORUS ELITE AX";
biosVendor = "American Megatrends Inc.";
biosVersion = "F20";
biosDate = "03/22/2025";
biosRelease = "2.0";
baseBoardVersion = "x.x";
baseBoardSerial = "SN220847001234";
serial = "SN220847001234";
socketPrefix = "AM5";
memory = {
manufacturer = "Crucial Technology";
partNumber = "CT16G56C46S5.M8G1";
speed = 5600;
count = 4;
};
};
macPrefix = "70:85:c2"; # ASRock OUI
};
# QEMU overlay with matching hardware strings
nixpkgs.overlays = [
(final: prev: {
qemu-stealth = prev.qemu-stealth.override {
edidManufacturer = "GSM";
edidSerial = "27GP850";
edidProductCode = "0x5bbf";
edidDpi = 109;
edidWeek = 38;
edidYear = 2023;
acpiOemId = "GBTNB ";
acpiOemTableId = "GBYTE ";
diskModel = "ST2000DM008-2UB102 ";
opticalModel = "ATAPI iHAS124 Y ";
};
})
];Two PowerShell scripts in guest/ handle cleanup and verification inside the Windows VM.
Removes QEMU/KVM/VirtIO registry artifacts left behind from before stealth was configured, or from VirtIO driver installation. Targets:
- SMBIOS registry entries (
HKLM:\HARDWARE\DESCRIPTION\System\BIOS) -- overwrites leaked QEMU/Bochs strings - VirtIO service keys (
CurrentControlSet\Services\VirtIO*,viostor,vioscsi,netkvm,Balloon) -- removes driver service remnants - QEMU/VirtIO PCI enumeration (
Enum\PCI\VEN_1AF4*,VEN_1B36*,VEN_1234*) -- removes cached device entries - QEMU Guest Agent service -- removes
QEMU Guest Agentservice key if present
When to run: Once, after applying host-side vfio-stealth-nix configuration for the first time, or after installing/removing VirtIO guest tools.
# Right-click PowerShell -> Run as Administrator
.\cleanup-registry.ps1
# Reboot after runningRead-only verification script that checks detection vectors from inside the Windows guest. Does NOT require Administrator. Checks:
Win32_ComputerSystem.Manufacturer-- flags QEMU, Bochs, VMware, Xen, KVMWin32_BIOS.Manufacturer-- flags SeaBIOS, Bochs BIOSWin32_BaseBoard.Manufacturer-- flags QEMU, Oracle, Microsoft- VM-specific Windows services -- VBoxService, VMTools, Hyper-V integration, QEMU GA
- PCI device vendor IDs -- VEN_1AF4 (VirtIO), VEN_1B36 (QEMU PCIe), VEN_1234 (QEMU VGA)
Win32_Fan-- should exist if SSDT loaded (WARN if missing)Win32_Battery-- should exist if emulated battery loaded (WARN if missing)Win32_PhysicalMemory-- should have DIMM info (WARN if missing)HypervisorPresent-- CPUID hypervisor bit (should be False)
Interpreting results:
- PASS -- vector is properly configured
- FAIL -- detection vector exposed, fix before running game security software
- WARN -- optional feature not active (ACPI SSDT tables may not be loaded)
.\verify-stealth.ps1
# Expected output: "0 failures, 0 warnings"The module exposes virtualisation.vfio-stealth.kernelPostPatch -- a shell script string that patches the kernel source tree via sed/awk. It combines BetterTiming (TSC compensation) and CPUID emulation (Hypervisor-Phantom) based on your config.
The patches target function signatures and symbol names, not line numbers, for resilience across kernel versions. CI validates anchors against nixpkgs latest kernel on every push.
boot.kernelPackages = pkgs.linuxPackagesFor (
pkgs.linuxPackages_cachyos.kernel.overrideAttrs (old: {
postPatch = (old.postPatch or "") + config.virtualisation.vfio-stealth.kernelPostPatch;
})
);boot.kernelPackages = pkgs.linuxPackagesFor (
pkgs.linux_latest.overrideAttrs (old: {
postPatch = (old.postPatch or "") + config.virtualisation.vfio-stealth.kernelPostPatch;
})
);BetterTiming (timing-patch.nix):
- Adds
last_exit_startandtotal_exit_timefields tostruct kvm_vcpu - Adds
total_exit_timetostruct kvm_vcpu; every stealth exit handler self-times withrdtsc()and accumulates only its own duration - Patches
MSR_IA32_TSCreads to return compensated (exit-time-subtracted) values - Enables RDTSC + RDTSCP interception in SVM
init_vmcb - Adds
handle_rdtsc_interceptionhandler that returns compensated TSC - Adds
handle_rdtscp_interceptionhandler returning compensated TSC + TSC_AUX in ECX - Wraps the CPUID, WBINVD, XSETBV and INVD exit handlers so each accumulates its own execution time
- Disables KVM hypercall instruction patching by forcing the
KVM_X86_QUIRK_FIX_HYPERCALL_INSNcheck true, so VMCALL/VMMCALL always injects #UD. This is host-wide: every VM on the host gets #UD, so Linux guests lose KVM paravirt features (kvmclock, PV TLB flush)
CPUID emulation (cpuid-patch.nix):
- Intercepts CPUID leaf 0 inside
svm_vcpu_runaftersvm_vcpu_enter_exitreturns - Overrides vendor string to
AuthenticAMDwith max leaf0x20 - Advances RIP and re-enters guest via
goto reenter_guest_fast(no full VM exit) - Clears RDTSC/RDTSCP interception bits (BetterTiming re-enables RDTSC if active)
CPUID passthrough (kernel/cpuid-disable.nix) — Exit-less CPUID:
- Clears
INTERCEPT_CPUIDininit_vmcbandpre_svm_run - Guest executes CPUID at native hardware speed (zero VM exit)
- AMD SVM loads guest XCR0 from VMCB — leaf 0xD naturally consistent
- Hardware returns AuthenticAMD with no hypervisor bit (synthetic, absent in real CPUID)
- Side effect: Hyper-V enlightenments invisible to guest (Windows uses TSC directly)
Both upstream techniques are vendored, not fetched at build time.
- AutoVirt (QEMU + EDK2 hardware-emulation patches). Its upstream was deleted around 2026-05; the patches live in
vendor/autovirtand the packages build from those.AMD-v11.1.0.patchwas ported here, not upstream. - SamuelTulach/BetterTiming (TSC compensation).
kernel/timing-patch.nixis a hand-port;version.jsonrecords the commit it was ported from.
update.yml runs scripts/update.sh on a daily cron. It watches the surviving AutoVirt forks for a patch newer than the vendored one, BetterTiming for a commit past the recorded port, and nixpkgs for a QEMU series no vendored patch covers, and files an issue when one appears. Adopting a change is a hand decision, see docs/RELEASE-PROCEDURE.md.
nix develop # dev shell with pre-commit hooks
nix flake check --no-build # eval check (fast)
nix build # build packages
nix fmt # format with treefmtThese represent current boundaries of software-level VM stealth:
| Limitation | Reason |
|---|---|
| Remote TPM attestation | Cloud-based attestation (e.g. Microsoft Azure Attestation) verifies TPM endorsement keys against manufacturer databases. swtpm keys are not manufacturer-signed and cannot pass remote attestation. No software-only fix. |
| VBS-dependent detection | Detection stacks requiring Virtualization-Based Security (VBS) + hypervisorlaunchtype=auto are incompatible with standard KVM guests. VBS means Windows must run under its own Hyper-V hypervisor. |
| Boot-time kernel drivers | Kernel-mode detection drivers that load before the OS can detect hypervisors via multiple vectors beyond CPUID. swtpm satisfies TPM 2.0 presence checks, but detection is independent of TPM state. |
PCIe root port vendor 1b36:000c |
QEMU's generic PCIe root port uses vendor 1b36 (Red Hat). No upstream override mechanism; patching the root port device type would need a QEMU source change. |
| fw_cfg I/O port probe | Reading 4 bytes from fw_cfg selector 0x0000 returns "QEMU". The DMA signature is patched but the legacy I/O path (ports 0x510/0x511) remains. Requires QEMU source patch to fully suppress. |
| Context-switch timing oracles | Detection libraries (VMAware v2.5+) use context-switch-based clocks independent of TSC. Claims immunity to all TSC/hardware clock spoofing including BetterTiming. An evolving area. |
| XSAVE state identification | Detection of CPUID interception handling via XCR0/XSS size discrepancies. On AMD SVM, VMRUN loads guest XCR0 from the VMCB, making leaf 0xD naturally consistent -- an evolving area. |
| NPT page-walk latency | Nested Page Table translation adds ~10-20ns per TLB miss. Detectable in theory via microbenchmarks, but high noise floor makes it impractical for false-positive rates. No known detection software uses this. |
| Performance variance | ML-based detection of frame-time jitter from VM exits. BetterTiming + cpuidPassthrough substantially reduce but cannot eliminate this surface. |
Of the 13 Hyper-V enlightenment features in this module, only
vendor_id and relaxed are unconditional (no KVM cap; libvirt-level
settings). The other 11 (vapic, spinlocks, frequencies,
vpindex, synic, stimer, reset, ipi, tlbflush,
reenlightenment, runtime) all require CONFIG_KVM_HYPERV=y in the
host kernel — a single switch in the kernel. The module's defaults
reflect this: the 11 KVM-gated features default off, the 2 truly
universal features default on. Enable additional features only after
verifying your kernel has CONFIG_KVM_HYPERV=y with
zcat /proc/config.gz | grep KVM_HYPERV. See
docs/OPTIONS.md for the full
opt-in table and the kernelCapabilities auto-detection helper.
GPL-2.0 (kernel patches mandate GPL)