spoof: class-bound nvme PCI vendor:device (extend 0018)

Override the qemu-nvme controller vendor:device (Red Hat 1b36:0010) to a real NVMe
vendor matched to the spoofed model brand (Samsung/WD/Kioxia/Kingston/Intel/SMI/
hynix/Micron/Phison), with a coherent subsystem. Safe: NVMe binds by class code,
not id, and this runs before nvme_init_ctrl so the IDENTIFY vid/ssvid stay aligned.
virtio/GPU vendor:device untouched (load-bearing). Inert without a seed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:35:47 +03:00
parent 9b17bdfa33
commit 3268fe11e1
4 changed files with 69 additions and 25 deletions
+30 -13
View File
@@ -1,17 +1,17 @@
qemu-spoof: OEM-brand emulated PCI subsystem ids (realize-time)
qemu-spoof: OEM-brand emulated PCI subsystem ids + class-bound nvme vendor:device
QEMU stamps a constant subsystem id (default 1b36:1100) on every emulated PCI
device -- a fleet fingerprint. The subsystem id is not used for guest driver
binding, so override it at realize time with an OEM-branded SVID/SSID (chipset
vendor + per-role device id) for non-bridge devices. Red Hat/virtio (0x1af4) is
skipped: legacy-virtio encodes the device type in the subsystem id, so changing
it would break the guest driver (virtio is de-fingerprinted by device choice
instead). vfio passthrough overwrites this with the real hardware SSID afterwards.
This is the realize-time override path the deferred 0018 note called for; it does
NOT touch vendor/device ids (those ARE driver-binding and would break drivers).
Inert unless a spoof-seed is set.
Realize-time overrides in do_pci_register_device (inert without a spoof-seed):
1. Subsystem id (SVID/SSID) of every non-bridge emulated device -> an OEM-branded
value. QEMU stamps a constant 1b36:1100 on all of them (a fleet fingerprint); the
subsystem id is not used for driver binding so this is safe. Red Hat/virtio
(0x1af4) is skipped -- legacy-virtio encodes the device type in the subsystem id.
2. The qemu-nvme controller (1b36:0010) vendor:device -> the real vendor:device of
the spoofed NVMe model brand (Samsung/WD/Kioxia/...), with a matching subsystem.
Safe because NVMe binds by class code, not id; runs before nvme_init_ctrl so the
IDENTIFY vid/ssvid stay coherent. virtio/GPU vendor:device are NOT touched (their
id IS the driver-binding contract). vfio passthrough overwrites with real hw ids.
diff --git a/hw/pci/pci.c b/hw/pci/pci.c
index 2c3657d..e3da2ab 100644
index 2c3657d..c6eb0ef 100644
--- a/hw/pci/pci.c
+++ b/hw/pci/pci.c
@@ -23,6 +23,7 @@
@@ -22,7 +22,7 @@ index 2c3657d..e3da2ab 100644
#include "qemu/datadir.h"
#include "qemu/units.h"
#include "hw/core/irq.h"
@@ -1417,6 +1418,27 @@ static PCIDevice *do_pci_register_device(PCIDevice *pci_dev,
@@ -1417,6 +1418,44 @@ static PCIDevice *do_pci_register_device(PCIDevice *pci_dev,
assert(!pc->subsystem_vendor_id);
assert(!pc->subsystem_id);
}
@@ -46,6 +46,23 @@ index 2c3657d..e3da2ab 100644
+ pci_set_word(pci_dev->config + PCI_SUBSYSTEM_ID,
+ spoof_pci_subdevice(role,
+ pci_get_word(pci_dev->config + PCI_SUBSYSTEM_ID)));
+ }
+ /*
+ * qemu-spoof: class-bound device ids may be set to a REAL vendor:device safely,
+ * because the guest binds these by class code, not by id. qemu-nvme (Red Hat
+ * 1b36:0010) -> the vendor:device of the spoofed NVMe model's brand, with a
+ * matching subsystem (a real NVMe controller's SSVID equals its VID). This runs
+ * before nvme_init_ctrl, so the NVMe IDENTIFY vid/ssvid stay coherent. virtio /
+ * GPU ids are NOT touched here (their id IS the driver-binding contract).
+ */
+ if (spoof_enabled() &&
+ !strcmp(object_get_typename(OBJECT(pci_dev)), "nvme")) {
+ uint16_t vid = spoof_nvme_pci_vendor(pci_get_word(pci_dev->config + PCI_VENDOR_ID));
+ uint16_t did = spoof_nvme_pci_device(pci_get_word(pci_dev->config + PCI_DEVICE_ID));
+ pci_config_set_vendor_id(pci_dev->config, vid);
+ pci_config_set_device_id(pci_dev->config, did);
+ pci_set_word(pci_dev->config + PCI_SUBSYSTEM_VENDOR_ID, vid);
+ pci_set_word(pci_dev->config + PCI_SUBSYSTEM_ID, did);
+ }
pci_init_cmask(pci_dev);
pci_init_wmask(pci_dev);
+11 -12
View File
@@ -20,30 +20,29 @@ match exactly. Naming: `0002` = infra, `0010+` = one aspect each.
| 0015-smbios-vm-bit | clear the SMBIOS type0 "VM" characteristic bit |
| 0016-edid-monitor | EDID vendor / name / model / serial / manufacture-date |
| 0017-storage-identity | IDE model / serial / fw / cdrom + NVMe model / serial / fw |
| 0018-pci-subsystem-id | realize-time OEM-brand of emulated PCI subsystem ids (SVID/SSID) |
| 0018-pci-subsystem-id | realize-time OEM-brand of PCI subsystem ids + class-bound nvme vendor:device |
| 0019-machine-desc | machine `desc` string → platform board model |
| 0021-smbios-identity | SMBIOS type3 chassis / type4 socket + cpu-mfr / type11 OEM / type17 memory |
| 0022-storage-extra | IDE WWN + rotation rate; NVMe EUI-64 + NGUID |
| 0023-cpu-microcode | CPU microcode revision (IA32_UCODE_REV), vendor-positioned |
| 0024-smbios-bios-type0 | SMBIOS type0 BIOS vendor / version / date (platform-anchored OEM firmware) |
## Why 0018 is subsystem-only (the PCI-id problem)
## The PCI-id problem (why 0018 is careful)
The PCI **vendor:device** id IS the guest driver-binding contract: NetKVM binds
`1af4:1000`, the ivshmem driver binds `1af4:1110`, the GPU driver binds its id.
Spoofing those breaks the device (no driver matches). So `0018` only rewrites the
**subsystem** id (not used for binding) — and even that skips `0x1af4`, because
*legacy*-virtio encodes the device type in the subsystem id. Class-bound devices
(NVMe / xHCI / AHCI bind by class code, not id) *could* take a real vendor:device,
but that needs a per-role allowlist. virtio / ivshmem / GPU ids are instead
de-fingerprinted by **device choice** (`e1000e` / SATA) and vfio `x-pci-*`, not by
id spoofing.
Spoofing those breaks the device (no driver matches). So `0018` rewrites the
**subsystem** id of all emulated devices (not used for binding; skips `0x1af4`
because legacy-virtio encodes the device type there), and overrides the full
**vendor:device** ONLY for class-bound controllers that bind by class code rather
than id — currently **qemu-nvme** → a real NVMe vendor:device matched to the spoofed
model brand. virtio / ivshmem / GPU ids are NOT touched (their id is load-bearing);
they are de-fingerprinted by **device choice** (`e1000e` / SATA) and vfio `x-pci-*`.
## Out of scope (handled by config or upstream, not these patches)
- **PCI vendor:device id** for class-bound devices (qemu-nvme `1b36:0010` → a real
NVMe vendor; `nec-usb-xhci`): safe in principle, would need a per-role allowlist in
the `0018` realize hook.
- **xHCI vendor:device**: also class-bound; prefer the `nec-usb-xhci` device (real
NEC id) via config over spoofing qemu-xhci. (qemu-nvme is handled by `0018`.)
- **HDA codec id** (`QEMU_HDA_ID_VENDOR 0x1af4`): a compile-time constant baked into a
`static const` codec descriptor; spoofing needs a runtime override of the
GET_PARAMETER verb response.
+26
View File
@@ -79,6 +79,20 @@ static uint64_t brand_oui(const char *m)
if (!strncmp(m, "CT", 2) || !strncmp(m, "Crucial", 7)) return 0x00A075; /* Micron */
return 0x0024E9;
}
/* Real NVMe controller PCI vendor:device, matched to the spoofed model's brand
* (the guest binds NVMe by class code, so this is safe to set on the QEMU ctrl). */
static void nvme_pci_id(const char *m, uint16_t *vid, uint16_t *did)
{
if (!strncmp(m, "Samsung", 7)) { *vid = 0x144d; *did = 0xa80a; } /* Samsung */
else if (!strncmp(m, "WD", 2)) { *vid = 0x15b7; *did = 0x5017; } /* WD/SanDisk */
else if (strstr(m, "TOSHIBA") || !strncmp(m, "KXG", 3)) { *vid = 0x1e0f; *did = 0x0001; } /* Kioxia */
else if (!strncmp(m, "KINGSTON", 8)){ *vid = 0x2646; *did = 0x5013; } /* Kingston */
else if (!strncmp(m, "INTEL", 5)) { *vid = 0x8086; *did = 0x390b; } /* Intel */
else if (!strncmp(m, "ADATA", 5)) { *vid = 0x126f; *did = 0x2263; } /* Silicon Motion */
else if (strstr(m, "Hynix")) { *vid = 0x1c5c; *did = 0x1d59; } /* SK hynix */
else if (!strncmp(m, "CT", 2) || !strncmp(m, "Micron", 6)) { *vid = 0x1344; *did = 0x5407; } /* Micron */
else { *vid = 0x1987; *did = 0x5012; } /* Phison */
}
static struct {
const char *model; char serial[24], fw[12]; uint64_t wwn; int rpm;
@@ -119,6 +133,18 @@ const char *spoof_nvme_model(const char *def) { if (!spoof_on()) return def; v_
const char *spoof_nvme_serial(const char *def) { if (!spoof_on()) return def; v_init(); return v.serial; }
const char *spoof_nvme_fw(const char *def) { if (!spoof_on()) return def; v_init(); return v.fw; }
uint64_t spoof_nvme_eui64(uint64_t def) { if (!spoof_on()) return def; v_init(); return v.eui64; }
uint16_t spoof_nvme_pci_vendor(uint16_t def)
{
if (!spoof_on()) return def;
v_init();
uint16_t vid, did; nvme_pci_id(v.model, &vid, &did); return vid;
}
uint16_t spoof_nvme_pci_device(uint16_t def)
{
if (!spoof_on()) return def;
v_init();
uint16_t vid, did; nvme_pci_id(v.model, &vid, &did); return did;
}
void spoof_nvme_nguid(uint8_t out[16])
{
+2
View File
@@ -92,6 +92,8 @@ const char *spoof_nvme_serial(const char *def);
const char *spoof_nvme_fw(const char *def); /* 8-char rev (fixes FR leak) */
uint64_t spoof_nvme_eui64(uint64_t def); /* OUI + vendor */
void spoof_nvme_nguid(uint8_t out[16]); /* filled iff enabled */
uint16_t spoof_nvme_pci_vendor(uint16_t def); /* class-bound: real NVMe vendor */
uint16_t spoof_nvme_pci_device(uint16_t def); /* brand-matched to the model */
const char *spoof_cdrom_model(const char *def);
int spoof_disk_rotation(int def); /* 1=SSD, else RPM (matches model) */