IGD does not come with a ROM BAR [1], the ROM BAR read by default from
kernel is actually the host VBIOS shadow RAM region that contains host
modifications on boot. With AI-assisted reverse engineering on VBIOS
binaries, it is observed that VBIOS saves BDSM register value on first
access and uses saved value if present.
When the image is executed in guest, since there is already a saved HPA
in VBIOS, it keeps using that value instead of the GPA programmed by
SeaBIOS in BDSM register in PCI config space, causing VBIOS to program
GTT entries with wrong address, resulting in garbled output in BIOS
POST and the error below detected by i915 driver.
i915 0000:00:02.0: [drm] *ERROR* Initial plane programming using invalid range, dma_addr=0x00000000db200000 ((null) [0x00000000baf00000-0x00000000beefffff])
The previous solution, c4c45e943e51 ("vfio/pci: Intel graphics legacy
mode assignment"), adjusts GTT entry addresses to (addr - host BDSM +
guest BDSM) to workaround that. But it is removed in 5aed8b0f0be2
("vfio/igd: Remove GTT write quirk in IO BAR 4") due to inconsistent
values in MMIO BAR0 and IO BAR4.
Considering it's unsafe to expose HPA to guest, a ROM quirk clearing
the saved value in VBIOS image is introduced. It searches the BDSM
accessor routine by matching a 19-byte signature anchored on the unique
`mov $0x105e,%ax` instruction, then locate the offset of saved BDSM and
clears it. This makes the routine fall through to the PCI config read
on the first call inside the guest.
The quirk is invoked in vfio_pci_load_rom(), and is gated on Gen 6-9
IGD devices with VGA access enabled and legacy (non-UEFI) PCIR code
type in the ROM header. A new trace event vfio_pci_igd_vbios_patched
is also introduced.
[1] 3.5.15, 4th Generation Intel Core Processor Family Datasheet Vol. 2
https://www.intel.com/content/dam/www/public/us/en/documents/datasheets/4th-gen-core-family-desktop-vol-2-datasheet.pdf
Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/3093
Reported-by: K S Maan <kirandeepmaan45@gmail.com>
Signed-off-by: Tomita Moeko <tomitamoeko@gmail.com>
Tested-by: K S Maan <kirandeepmaan45@gmail.com>
---
hw/vfio/igd-stubs.c | 5 ++
hw/vfio/igd.c | 106 +++++++++++++++++++++++++++++++++++++++++++
hw/vfio/pci-quirks.c | 5 ++
hw/vfio/pci.c | 2 +
hw/vfio/pci.h | 3 ++
hw/vfio/trace-events | 1 +
6 files changed, 122 insertions(+)
diff --git a/hw/vfio/igd-stubs.c b/hw/vfio/igd-stubs.c
index f7687d9091..879a8aff56 100644
--- a/hw/vfio/igd-stubs.c
+++ b/hw/vfio/igd-stubs.c
@@ -18,3 +18,8 @@ bool vfio_probe_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp)
{
return true;
}
+
+void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev)
+{
+ return;
+}
diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c
index 17437ae18d..e00f6f8315 100644
--- a/hw/vfio/igd.c
+++ b/hw/vfio/igd.c
@@ -739,3 +739,109 @@ bool vfio_probe_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp)
return vfio_pci_igd_config_quirk(vdev, errp);
}
+
+/*
+ * IGD ROM BAR read from kernel is actually the host VBIOS shadow RAM region,
+ * which contains host modifications. In Gen 6-9 VBIOS, the routine below is
+ * used to get BDSM value when programming the initial GTT.
+ * xx xx xx xx v: .long ? # saved value
+ * 66 53 push %ebx
+ * 66 2e 83 3e xx xx 00 cmpl $0x0,%cs:v # is saved value empty?
+ * 74 07 je 1f # if zero, go compute
+ * 66 2e a1 xx xx mov %cs:v,%eax # else return saved value
+ * eb 0f jmp 2f
+ * b8 5e 10 1: mov $0x105e,%ax # dev 00:02.0, offset 5E
+ * e8 xx xx call pci_read_cfg_word
+ * 66 c1 e0 10 shl $0x10,%eax # left shift 16 bits
+ * 66 2e a3 xx xx mov %eax,%cs:v # save the result
+ * 66 5b 2:pop %ebx
+ * c3 ret
+ * When running the VBIOS in guest, saved value still reflects the host stolen
+ * memory base address, which is not correct in guest. So we need to patch the
+ * VBIOS to clear the saved value.
+ *
+ * The unique 19-byte starts at `cmpl $0,%cs:v` and ends at `mov $0x105e,%ax`
+ * anchors the match to the routine. Both `cs:` displacements must reference
+ * the same offset.
+ */
+static int igd_vbios_find_saved_bdsm(const uint8_t *rom, size_t rom_size,
+ uint16_t *bdsm_offset)
+{
+ static const uint8_t start[] = { 0x66, 0x2e, 0x83, 0x3e };
+ static const uint8_t middle[] = { 0x00, 0x74, 0x07, 0x66, 0x2e, 0xa1 };
+ static const uint8_t end[] = { 0xeb, 0x0f, 0xb8, 0x5e, 0x10 };
+ size_t i;
+ bool found = false;
+
+ if (rom_size < 19) {
+ return -ENOENT;
+ }
+
+ for (i = 0; i + 19 <= rom_size; i++) {
+ if (memcmp(rom + i, start, sizeof(start)) != 0 ||
+ memcmp(rom + i + 6, middle, sizeof(middle)) != 0 ||
+ memcmp(rom + i + 14, end, sizeof(end)) != 0) {
+ continue;
+ }
+
+ /* same saved value address? */
+ if (rom[i + 4] != rom[i + 12] || rom[i + 5] != rom[i + 13]) {
+ continue;
+ }
+
+ if (found) {
+ return -EEXIST;
+ }
+
+ *bdsm_offset = rom[i + 4] | ((uint16_t)rom[i + 5] << 8);
+ found = true;
+ }
+
+ if (!found) {
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev)
+{
+ int ret, gen;
+ uint16_t pcir_offset, bdsm_offset = 0;
+ uint8_t checksum;
+
+ if (!vfio_pci_is(vdev, PCI_VENDOR_ID_INTEL, PCI_ANY_ID) ||
+ !vfio_is_vga(vdev) || !vdev->vga) {
+ return;
+ }
+
+ /* Only Gen 6~9 devices have legacy VBIOS as Option ROM */
+ gen = igd_gen(vdev);
+ if (gen < 6 || gen > 9) {
+ return;
+ }
+
+ if (pci_get_word(vdev->rom) != 0xaa55) {
+ return;
+ }
+
+ /* Must be a legacy ROM */
+ pcir_offset = pci_get_word(vdev->rom + 0x18);
+ if (pcir_offset >= vdev->rom_size ||
+ memcmp(vdev->rom + pcir_offset, "PCIR", 4) ||
+ pci_get_byte(vdev->rom + pcir_offset + 0x14) != 0x00) {
+ return;
+ }
+
+ ret = igd_vbios_find_saved_bdsm(vdev->rom, vdev->rom_size, &bdsm_offset);
+ if (ret < 0) {
+ return;
+ }
+
+ memset(vdev->rom + bdsm_offset, 0, sizeof(uint32_t));
+
+ checksum = pci_rom_calculate_checksum(vdev->rom, vdev->rom_size);
+ ((uint8_t *)vdev->rom)[6] = checksum;
+
+ trace_vfio_pci_igd_vbios_patched(vdev->vbasedev.name);
+}
diff --git a/hw/vfio/pci-quirks.c b/hw/vfio/pci-quirks.c
index bccf31751f..45db968681 100644
--- a/hw/vfio/pci-quirks.c
+++ b/hw/vfio/pci-quirks.c
@@ -1592,3 +1592,8 @@ bool vfio_add_virt_caps(VFIOPCIDevice *vdev, Error **errp)
return true;
}
+
+void vfio_rom_quirk_setup(VFIOPCIDevice *vdev)
+{
+ vfio_probe_igd_legacy_rom_quirk(vdev);
+}
diff --git a/hw/vfio/pci.c b/hw/vfio/pci.c
index 6cbd65126e..66d6315e6f 100644
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -1088,6 +1088,8 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
if (pdev->rom_need_patch_id) {
pci_rom_patch_ids(pdev, vdev->rom, vdev->rom_size);
}
+
+ vfio_rom_quirk_setup(vdev);
}
/* "Raw" read of underlying config space. */
diff --git a/hw/vfio/pci.h b/hw/vfio/pci.h
index c3a1f53d35..d8d6c09632 100644
--- a/hw/vfio/pci.h
+++ b/hw/vfio/pci.h
@@ -251,10 +251,13 @@ void vfio_bar_quirk_exit(VFIOPCIDevice *vdev, int nr);
void vfio_bar_quirk_finalize(VFIOPCIDevice *vdev, int nr);
void vfio_setup_resetfn_quirk(VFIOPCIDevice *vdev);
bool vfio_add_virt_caps(VFIOPCIDevice *vdev, Error **errp);
+void vfio_rom_quirk_setup(VFIOPCIDevice *vdev);
void vfio_quirk_reset(VFIOPCIDevice *vdev);
VFIOQuirk *vfio_quirk_alloc(int nr_mem);
+
void vfio_probe_igd_bar0_quirk(VFIOPCIDevice *vdev, int nr);
bool vfio_probe_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp);
+void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev);
extern const PropertyInfo qdev_prop_nv_gpudirect_clique;
diff --git a/hw/vfio/trace-events b/hw/vfio/trace-events
index 2049159015..7dc334ccb3 100644
--- a/hw/vfio/trace-events
+++ b/hw/vfio/trace-events
@@ -90,6 +90,7 @@ vfio_pci_igd_bar4_write(const char *name, uint32_t index, uint32_t data, uint32_
vfio_pci_igd_bdsm_enabled(const char *name, int size) "%s %dMB"
vfio_pci_igd_host_bridge_enabled(const char *name) "%s"
vfio_pci_igd_lpc_bridge_enabled(const char *name) "%s"
+vfio_pci_igd_vbios_patched(const char *name) "%s"
# listener.c
vfio_iommu_map_notify(const char *op, uint64_t iova_start, uint64_t iova_end) "iommu %s @ 0x%"PRIx64" - 0x%"PRIx64
--
2.53.0
On Thu, 4 Jun 2026 01:33:54 +0800
Tomita Moeko <tomitamoeko@gmail.com> wrote:
> diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c
> index 17437ae18d..e00f6f8315 100644
> --- a/hw/vfio/igd.c
> +++ b/hw/vfio/igd.c
> @@ -739,3 +739,109 @@ bool vfio_probe_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp)
>
> return vfio_pci_igd_config_quirk(vdev, errp);
> }
> +
> +/*
> + * IGD ROM BAR read from kernel is actually the host VBIOS shadow RAM region,
> + * which contains host modifications. In Gen 6-9 VBIOS, the routine below is
> + * used to get BDSM value when programming the initial GTT.
> + * xx xx xx xx v: .long ? # saved value
> + * 66 53 push %ebx
> + * 66 2e 83 3e xx xx 00 cmpl $0x0,%cs:v # is saved value empty?
> + * 74 07 je 1f # if zero, go compute
> + * 66 2e a1 xx xx mov %cs:v,%eax # else return saved value
> + * eb 0f jmp 2f
> + * b8 5e 10 1: mov $0x105e,%ax # dev 00:02.0, offset 5E
> + * e8 xx xx call pci_read_cfg_word
> + * 66 c1 e0 10 shl $0x10,%eax # left shift 16 bits
> + * 66 2e a3 xx xx mov %eax,%cs:v # save the result
> + * 66 5b 2:pop %ebx
> + * c3 ret
> + * When running the VBIOS in guest, saved value still reflects the host stolen
> + * memory base address, which is not correct in guest. So we need to patch the
> + * VBIOS to clear the saved value.
> + *
> + * The unique 19-byte starts at `cmpl $0,%cs:v` and ends at `mov $0x105e,%ax`
> + * anchors the match to the routine. Both `cs:` displacements must reference
> + * the same offset.
> + */
Neat, cool quirk.
> +static int igd_vbios_find_saved_bdsm(const uint8_t *rom, size_t rom_size,
> + uint16_t *bdsm_offset)
> +{
> + static const uint8_t start[] = { 0x66, 0x2e, 0x83, 0x3e };
> + static const uint8_t middle[] = { 0x00, 0x74, 0x07, 0x66, 0x2e, 0xa1 };
> + static const uint8_t end[] = { 0xeb, 0x0f, 0xb8, 0x5e, 0x10 };
> + size_t i;
> + bool found = false;
> +
> + if (rom_size < 19) {
> + return -ENOENT;
> + }
> +
> + for (i = 0; i + 19 <= rom_size; i++) {
> + if (memcmp(rom + i, start, sizeof(start)) != 0 ||
> + memcmp(rom + i + 6, middle, sizeof(middle)) != 0 ||
> + memcmp(rom + i + 14, end, sizeof(end)) != 0) {
> + continue;
> + }
> +
> + /* same saved value address? */
> + if (rom[i + 4] != rom[i + 12] || rom[i + 5] != rom[i + 13]) {
> + continue;
> + }
> +
> + if (found) {
> + return -EEXIST;
> + }
> +
> + *bdsm_offset = rom[i + 4] | ((uint16_t)rom[i + 5] << 8);
This needs a bounds check to make sure it's still within the ROM,
either here or before the memset().
> + found = true;
> + }
> +
> + if (!found) {
> + return -ENOENT;
> + }
> +
> + return 0;
> +}
> +
> +void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev)
> +{
> + int ret, gen;
> + uint16_t pcir_offset, bdsm_offset = 0;
> + uint8_t checksum;
> +
> + if (!vfio_pci_is(vdev, PCI_VENDOR_ID_INTEL, PCI_ANY_ID) ||
> + !vfio_is_vga(vdev) || !vdev->vga) {
> + return;
> + }
> +
> + /* Only Gen 6~9 devices have legacy VBIOS as Option ROM */
> + gen = igd_gen(vdev);
> + if (gen < 6 || gen > 9) {
> + return;
> + }
> +
> + if (pci_get_word(vdev->rom) != 0xaa55) {
> + return;
> + }
> +
> + /* Must be a legacy ROM */
> + pcir_offset = pci_get_word(vdev->rom + 0x18);
> + if (pcir_offset >= vdev->rom_size ||
This gates the below test to pcir_offset + 0x14, so should be:
if (pcir_offset + 0x14 >= vdev->rom_size ||
> + memcmp(vdev->rom + pcir_offset, "PCIR", 4) ||
> + pci_get_byte(vdev->rom + pcir_offset + 0x14) != 0x00) {
> + return;
> + }
> +
> + ret = igd_vbios_find_saved_bdsm(vdev->rom, vdev->rom_size, &bdsm_offset);
> + if (ret < 0) {
> + return;
> + }
> +
> + memset(vdev->rom + bdsm_offset, 0, sizeof(uint32_t));
> +
> + checksum = pci_rom_calculate_checksum(vdev->rom, vdev->rom_size);
> + ((uint8_t *)vdev->rom)[6] = checksum;
If updated per previous recommendation, this becomes:
((uint8_t *)vdev->rom)[6] -= checksum;
Thanks,
Alex
Hi, as these patches fixes legacy mode grabled grub screen they add
regression to UEFI guest where monitor won't woke up until grub timeout.
CMD used : -device
vfio-pci,host=00:02.0,id=igpu,addr=2.0,x-igd-legacy-mode=off,x-igd-lpc=on,romfile=/home/maan/QemuVMs/MyGopRomfiles/InteligdGopHD4600.rom
romfile is patched with intelgopdriver.efi from VfioIgdPkg repo.
Here is what i915 logs say in guest with patched qemu :
[ 10.823656] i915 0000:00:02.0: [drm] Found haswell (device ID 0412)
integrated display version 7.00 stepping N/A
[ 10.824300] i915 0000:00:02.0: [drm] VT-d active for gfx access
[ 10.824763] i915 0000:00:02.0: vgaarb: deactivate vga console
[ 10.824867] i915 0000:00:02.0: [drm] Using Transparent Hugepages
[ 10.824871] i915 0000:00:02.0: [drm] DMAR active, disabling use of
stolen memory
[ 10.832111] i915 0000:00:02.0: Invalid PCI ROM header signature:
expecting 0xaa55, got 0x0000
[ 10.832118] i915 0000:00:02.0: [drm] Failed to find VBIOS tables (VBT)
[ 10.832154] i915 0000:00:02.0: vgaarb: VGA decodes changed:
olddecodes=io+mem,decodes=io+mem:owns=io+mem
[ 10.842837] i915 0000:00:02.0: [drm] Registered 3 planes with drm panic
[ 10.850377] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 0
[ 11.678013] fbcon: i915drmfb (fb0) is primary device
[ 11.748610] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
So look like VBT table is not expose to guest.
These are the logs of stock qemu with working grub and all :
[ 28.945622] i915 0000:00:02.0: [drm] Found haswell (device ID 0412)
integrated display version 7.00 stepping N/A
[ 28.946393] i915 0000:00:02.0: [drm] VT-d active for gfx access
[ 28.961770] i915 0000:00:02.0: vgaarb: deactivate vga console
[ 28.961830] i915 0000:00:02.0: [drm] Using Transparent Hugepages
[ 28.961833] i915 0000:00:02.0: [drm] DMAR active, disabling use of
stolen memory
[ 29.016673] i915 0000:00:02.0: vgaarb: VGA decodes changed:
olddecodes=io+mem,decodes=io+mem:owns=io+mem
[ 29.051910] i915 0000:00:02.0: [drm] Registered 3 planes with drm panic
[ 29.055360] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 1
[ 29.903202] fbcon: i915drmfb (fb0) is primary device
[ 29.990056] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
Regards,
K S Maan
On 6/6/26 3:32 AM, Alex Williamson wrote:
> On Thu, 4 Jun 2026 01:33:54 +0800
> Tomita Moeko<tomitamoeko@gmail.com> wrote:
>> diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c
>> index 17437ae18d..e00f6f8315 100644
>> --- a/hw/vfio/igd.c
>> +++ b/hw/vfio/igd.c
>> @@ -739,3 +739,109 @@ bool vfio_probe_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp)
>>
>> return vfio_pci_igd_config_quirk(vdev, errp);
>> }
>> +
>> +/*
>> + * IGD ROM BAR read from kernel is actually the host VBIOS shadow RAM region,
>> + * which contains host modifications. In Gen 6-9 VBIOS, the routine below is
>> + * used to get BDSM value when programming the initial GTT.
>> + * xx xx xx xx v: .long ? # saved value
>> + * 66 53 push %ebx
>> + * 66 2e 83 3e xx xx 00 cmpl $0x0,%cs:v # is saved value empty?
>> + * 74 07 je 1f # if zero, go compute
>> + * 66 2e a1 xx xx mov %cs:v,%eax # else return saved value
>> + * eb 0f jmp 2f
>> + * b8 5e 10 1: mov $0x105e,%ax # dev 00:02.0, offset 5E
>> + * e8 xx xx call pci_read_cfg_word
>> + * 66 c1 e0 10 shl $0x10,%eax # left shift 16 bits
>> + * 66 2e a3 xx xx mov %eax,%cs:v # save the result
>> + * 66 5b 2:pop %ebx
>> + * c3 ret
>> + * When running the VBIOS in guest, saved value still reflects the host stolen
>> + * memory base address, which is not correct in guest. So we need to patch the
>> + * VBIOS to clear the saved value.
>> + *
>> + * The unique 19-byte starts at `cmpl $0,%cs:v` and ends at `mov $0x105e,%ax`
>> + * anchors the match to the routine. Both `cs:` displacements must reference
>> + * the same offset.
>> + */
> Neat, cool quirk.
>
>> +static int igd_vbios_find_saved_bdsm(const uint8_t *rom, size_t rom_size,
>> + uint16_t *bdsm_offset)
>> +{
>> + static const uint8_t start[] = { 0x66, 0x2e, 0x83, 0x3e };
>> + static const uint8_t middle[] = { 0x00, 0x74, 0x07, 0x66, 0x2e, 0xa1 };
>> + static const uint8_t end[] = { 0xeb, 0x0f, 0xb8, 0x5e, 0x10 };
>> + size_t i;
>> + bool found = false;
>> +
>> + if (rom_size < 19) {
>> + return -ENOENT;
>> + }
>> +
>> + for (i = 0; i + 19 <= rom_size; i++) {
>> + if (memcmp(rom + i, start, sizeof(start)) != 0 ||
>> + memcmp(rom + i + 6, middle, sizeof(middle)) != 0 ||
>> + memcmp(rom + i + 14, end, sizeof(end)) != 0) {
>> + continue;
>> + }
>> +
>> + /* same saved value address? */
>> + if (rom[i + 4] != rom[i + 12] || rom[i + 5] != rom[i + 13]) {
>> + continue;
>> + }
>> +
>> + if (found) {
>> + return -EEXIST;
>> + }
>> +
>> + *bdsm_offset = rom[i + 4] | ((uint16_t)rom[i + 5] << 8);
> This needs a bounds check to make sure it's still within the ROM,
> either here or before the memset().
>
>> + found = true;
>> + }
>> +
>> + if (!found) {
>> + return -ENOENT;
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev)
>> +{
>> + int ret, gen;
>> + uint16_t pcir_offset, bdsm_offset = 0;
>> + uint8_t checksum;
>> +
>> + if (!vfio_pci_is(vdev, PCI_VENDOR_ID_INTEL, PCI_ANY_ID) ||
>> + !vfio_is_vga(vdev) || !vdev->vga) {
>> + return;
>> + }
>> +
>> + /* Only Gen 6~9 devices have legacy VBIOS as Option ROM */
>> + gen = igd_gen(vdev);
>> + if (gen < 6 || gen > 9) {
>> + return;
>> + }
>> +
>> + if (pci_get_word(vdev->rom) != 0xaa55) {
>> + return;
>> + }
>> +
>> + /* Must be a legacy ROM */
>> + pcir_offset = pci_get_word(vdev->rom + 0x18);
>> + if (pcir_offset >= vdev->rom_size ||
> This gates the below test to pcir_offset + 0x14, so should be:
>
> if (pcir_offset + 0x14 >= vdev->rom_size ||
>
>> + memcmp(vdev->rom + pcir_offset, "PCIR", 4) ||
>> + pci_get_byte(vdev->rom + pcir_offset + 0x14) != 0x00) {
>> + return;
>> + }
>> +
>> + ret = igd_vbios_find_saved_bdsm(vdev->rom, vdev->rom_size, &bdsm_offset);
>> + if (ret < 0) {
>> + return;
>> + }
>> +
>> + memset(vdev->rom + bdsm_offset, 0, sizeof(uint32_t));
>> +
>> + checksum = pci_rom_calculate_checksum(vdev->rom, vdev->rom_size);
>> + ((uint8_t *)vdev->rom)[6] = checksum;
> If updated per previous recommendation, this becomes:
>
> ((uint8_t *)vdev->rom)[6] -= checksum;
>
> Thanks,
> Alex
>
Hi, as these patches fixes legacy mode grabled grub screen they add
regression to UEFI guest where monitor won't woke up until grub timeout.
CMD used : -device
vfio-pci,host=00:02.0,id=igpu,addr=2.0,x-igd-legacy-mode=off,x-igd-lpc=on,romfile=/home/maan/QemuVMs/MyGopRomfiles/InteligdGopHD4600.rom
romfile is patched with intelgopdriver.efi from VfioIgdPkg repo.
Here is what i915 logs say in guest with patched qemu :
[ 10.823656] i915 0000:00:02.0: [drm] Found haswell (device ID 0412)
integrated display version 7.00 stepping N/A
[ 10.824300] i915 0000:00:02.0: [drm] VT-d active for gfx access
[ 10.824763] i915 0000:00:02.0: vgaarb: deactivate vga console
[ 10.824867] i915 0000:00:02.0: [drm] Using Transparent Hugepages
[ 10.824871] i915 0000:00:02.0: [drm] DMAR active, disabling use of
stolen memory
[ 10.832111] i915 0000:00:02.0: Invalid PCI ROM header signature:
expecting 0xaa55, got 0x0000
[ 10.832118] i915 0000:00:02.0: [drm] Failed to find VBIOS tables (VBT)
[ 10.832154] i915 0000:00:02.0: vgaarb: VGA decodes changed:
olddecodes=io+mem,decodes=io+mem:owns=io+mem
[ 10.842837] i915 0000:00:02.0: [drm] Registered 3 planes with drm panic
[ 10.850377] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 0
[ 11.678013] fbcon: i915drmfb (fb0) is primary device
[ 11.748610] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
So look like VBT table is not expose to guest.
These are the logs of stock qemu with working grub and all :
[ 28.945622] i915 0000:00:02.0: [drm] Found haswell (device ID 0412)
integrated display version 7.00 stepping N/A
[ 28.946393] i915 0000:00:02.0: [drm] VT-d active for gfx access
[ 28.961770] i915 0000:00:02.0: vgaarb: deactivate vga console
[ 28.961830] i915 0000:00:02.0: [drm] Using Transparent Hugepages
[ 28.961833] i915 0000:00:02.0: [drm] DMAR active, disabling use of
stolen memory
[ 29.016673] i915 0000:00:02.0: vgaarb: VGA decodes changed:
olddecodes=io+mem,decodes=io+mem:owns=io+mem
[ 29.051910] i915 0000:00:02.0: [drm] Registered 3 planes with drm panic
[ 29.055360] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 1
[ 29.903202] fbcon: i915drmfb (fb0) is primary device
[ 29.990056] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
Regards,
K S Maan
On Sat, 6 Jun 2026 at 03:32, Alex Williamson <alex@shazbot.org> wrote:
> On Thu, 4 Jun 2026 01:33:54 +0800
> Tomita Moeko <tomitamoeko@gmail.com> wrote:
> > diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c
> > index 17437ae18d..e00f6f8315 100644
> > --- a/hw/vfio/igd.c
> > +++ b/hw/vfio/igd.c
> > @@ -739,3 +739,109 @@ bool vfio_probe_igd_config_quirk(VFIOPCIDevice
> *vdev, Error **errp)
> >
> > return vfio_pci_igd_config_quirk(vdev, errp);
> > }
> > +
> > +/*
> > + * IGD ROM BAR read from kernel is actually the host VBIOS shadow RAM
> region,
> > + * which contains host modifications. In Gen 6-9 VBIOS, the routine
> below is
> > + * used to get BDSM value when programming the initial GTT.
> > + * xx xx xx xx v: .long ? # saved value
> > + * 66 53 push %ebx
> > + * 66 2e 83 3e xx xx 00 cmpl $0x0,%cs:v # is saved value
> empty?
> > + * 74 07 je 1f # if zero, go
> compute
> > + * 66 2e a1 xx xx mov %cs:v,%eax # else return
> saved value
> > + * eb 0f jmp 2f
> > + * b8 5e 10 1: mov $0x105e,%ax # dev 00:02.0,
> offset 5E
> > + * e8 xx xx call pci_read_cfg_word
> > + * 66 c1 e0 10 shl $0x10,%eax # left shift 16
> bits
> > + * 66 2e a3 xx xx mov %eax,%cs:v # save the result
> > + * 66 5b 2:pop %ebx
> > + * c3 ret
> > + * When running the VBIOS in guest, saved value still reflects the host
> stolen
> > + * memory base address, which is not correct in guest. So we need to
> patch the
> > + * VBIOS to clear the saved value.
> > + *
> > + * The unique 19-byte starts at `cmpl $0,%cs:v` and ends at `mov
> $0x105e,%ax`
> > + * anchors the match to the routine. Both `cs:` displacements must
> reference
> > + * the same offset.
> > + */
>
> Neat, cool quirk.
>
> > +static int igd_vbios_find_saved_bdsm(const uint8_t *rom, size_t
> rom_size,
> > + uint16_t *bdsm_offset)
> > +{
> > + static const uint8_t start[] = { 0x66, 0x2e, 0x83, 0x3e };
> > + static const uint8_t middle[] = { 0x00, 0x74, 0x07, 0x66, 0x2e,
> 0xa1 };
> > + static const uint8_t end[] = { 0xeb, 0x0f, 0xb8, 0x5e, 0x10 };
> > + size_t i;
> > + bool found = false;
> > +
> > + if (rom_size < 19) {
> > + return -ENOENT;
> > + }
> > +
> > + for (i = 0; i + 19 <= rom_size; i++) {
> > + if (memcmp(rom + i, start, sizeof(start)) != 0 ||
> > + memcmp(rom + i + 6, middle, sizeof(middle)) != 0 ||
> > + memcmp(rom + i + 14, end, sizeof(end)) != 0) {
> > + continue;
> > + }
> > +
> > + /* same saved value address? */
> > + if (rom[i + 4] != rom[i + 12] || rom[i + 5] != rom[i + 13]) {
> > + continue;
> > + }
> > +
> > + if (found) {
> > + return -EEXIST;
> > + }
> > +
> > + *bdsm_offset = rom[i + 4] | ((uint16_t)rom[i + 5] << 8);
>
> This needs a bounds check to make sure it's still within the ROM,
> either here or before the memset().
>
> > + found = true;
> > + }
> > +
> > + if (!found) {
> > + return -ENOENT;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev)
> > +{
> > + int ret, gen;
> > + uint16_t pcir_offset, bdsm_offset = 0;
> > + uint8_t checksum;
> > +
> > + if (!vfio_pci_is(vdev, PCI_VENDOR_ID_INTEL, PCI_ANY_ID) ||
> > + !vfio_is_vga(vdev) || !vdev->vga) {
> > + return;
> > + }
> > +
> > + /* Only Gen 6~9 devices have legacy VBIOS as Option ROM */
> > + gen = igd_gen(vdev);
> > + if (gen < 6 || gen > 9) {
> > + return;
> > + }
> > +
> > + if (pci_get_word(vdev->rom) != 0xaa55) {
> > + return;
> > + }
> > +
> > + /* Must be a legacy ROM */
> > + pcir_offset = pci_get_word(vdev->rom + 0x18);
> > + if (pcir_offset >= vdev->rom_size ||
>
> This gates the below test to pcir_offset + 0x14, so should be:
>
> if (pcir_offset + 0x14 >= vdev->rom_size ||
>
> > + memcmp(vdev->rom + pcir_offset, "PCIR", 4) ||
> > + pci_get_byte(vdev->rom + pcir_offset + 0x14) != 0x00) {
> > + return;
> > + }
> > +
> > + ret = igd_vbios_find_saved_bdsm(vdev->rom, vdev->rom_size,
> &bdsm_offset);
> > + if (ret < 0) {
> > + return;
> > + }
> > +
> > + memset(vdev->rom + bdsm_offset, 0, sizeof(uint32_t));
> > +
> > + checksum = pci_rom_calculate_checksum(vdev->rom, vdev->rom_size);
> > + ((uint8_t *)vdev->rom)[6] = checksum;
>
> If updated per previous recommendation, this becomes:
>
> ((uint8_t *)vdev->rom)[6] -= checksum;
>
> Thanks,
> Alex
>
Found the root cause is the vendor/device id fixup in option rom
header. EFI requires the signature in ROM header at 0x04 being
0x00000ef1, the new pci_patch_ids() breaks it as it overwrites
checksum at 0x06. Given that EFI does not require either the IDs
in header nor the checksum, will skip the fixup for EFI roms in
v2.
Thanks,
Moeko
On 2026-06-08 10:17, Kirandeep Maan wrote:
> Hi, as these patches fixes legacy mode grabled grub screen they add regression to UEFI guest where monitor won't woke up until grub timeout.
>
> CMD used : -device vfio-pci,host=00:02.0,id=igpu,addr=2.0,x-igd-legacy-mode=off,x-igd-lpc=on,romfile=/home/maan/QemuVMs/MyGopRomfiles/InteligdGopHD4600.rom
>
> romfile is patched with intelgopdriver.efi from VfioIgdPkg repo.
>
> Here is what i915 logs say in guest with patched qemu :
> [ 10.823656] i915 0000:00:02.0: [drm] Found haswell (device ID 0412) integrated display version 7.00 stepping N/A
> [ 10.824300] i915 0000:00:02.0: [drm] VT-d active for gfx access
> [ 10.824763] i915 0000:00:02.0: vgaarb: deactivate vga console
> [ 10.824867] i915 0000:00:02.0: [drm] Using Transparent Hugepages
> [ 10.824871] i915 0000:00:02.0: [drm] DMAR active, disabling use of stolen memory
> [ 10.832111] i915 0000:00:02.0: Invalid PCI ROM header signature: expecting 0xaa55, got 0x0000
> [ 10.832118] i915 0000:00:02.0: [drm] Failed to find VBIOS tables (VBT)
> [ 10.832154] i915 0000:00:02.0: vgaarb: VGA decodes changed: olddecodes=io+mem,decodes=io+mem:owns=io+mem
> [ 10.842837] i915 0000:00:02.0: [drm] Registered 3 planes with drm panic
> [ 10.850377] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 0
> [ 11.678013] fbcon: i915drmfb (fb0) is primary device
> [ 11.748610] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
>
> So look like VBT table is not expose to guest.
>
> These are the logs of stock qemu with working grub and all :
> [ 28.945622] i915 0000:00:02.0: [drm] Found haswell (device ID 0412) integrated display version 7.00 stepping N/A
> [ 28.946393] i915 0000:00:02.0: [drm] VT-d active for gfx access
> [ 28.961770] i915 0000:00:02.0: vgaarb: deactivate vga console
> [ 28.961830] i915 0000:00:02.0: [drm] Using Transparent Hugepages
> [ 28.961833] i915 0000:00:02.0: [drm] DMAR active, disabling use of stolen memory
> [ 29.016673] i915 0000:00:02.0: vgaarb: VGA decodes changed: olddecodes=io+mem,decodes=io+mem:owns=io+mem
> [ 29.051910] i915 0000:00:02.0: [drm] Registered 3 planes with drm panic
> [ 29.055360] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 1
> [ 29.903202] fbcon: i915drmfb (fb0) is primary device
> [ 29.990056] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
>
> Regards,
> K S Maan
>
> On Sat, 6 Jun 2026 at 03:32, Alex Williamson <alex@shazbot.org <mailto:alex@shazbot.org>> wrote:
>
> On Thu, 4 Jun 2026 01:33:54 +0800
> Tomita Moeko <tomitamoeko@gmail.com <mailto:tomitamoeko@gmail.com>> wrote:
> > diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c
> > index 17437ae18d..e00f6f8315 100644
> > --- a/hw/vfio/igd.c
> > +++ b/hw/vfio/igd.c
> > @@ -739,3 +739,109 @@ bool vfio_probe_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp)
> >
> > return vfio_pci_igd_config_quirk(vdev, errp);
> > }
> > +
> > +/*
> > + * IGD ROM BAR read from kernel is actually the host VBIOS shadow RAM region,
> > + * which contains host modifications. In Gen 6-9 VBIOS, the routine below is
> > + * used to get BDSM value when programming the initial GTT.
> > + * xx xx xx xx v: .long ? # saved value
> > + * 66 53 push %ebx
> > + * 66 2e 83 3e xx xx 00 cmpl $0x0,%cs:v # is saved value empty?
> > + * 74 07 je 1f # if zero, go compute
> > + * 66 2e a1 xx xx mov %cs:v,%eax # else return saved value
> > + * eb 0f jmp 2f
> > + * b8 5e 10 1: mov $0x105e,%ax # dev 00:02.0, offset 5E
> > + * e8 xx xx call pci_read_cfg_word
> > + * 66 c1 e0 10 shl $0x10,%eax # left shift 16 bits
> > + * 66 2e a3 xx xx mov %eax,%cs:v # save the result
> > + * 66 5b 2:pop %ebx
> > + * c3 ret
> > + * When running the VBIOS in guest, saved value still reflects the host stolen
> > + * memory base address, which is not correct in guest. So we need to patch the
> > + * VBIOS to clear the saved value.
> > + *
> > + * The unique 19-byte starts at `cmpl $0,%cs:v` and ends at `mov $0x105e,%ax`
> > + * anchors the match to the routine. Both `cs:` displacements must reference
> > + * the same offset.
> > + */
>
> Neat, cool quirk.
>
> > +static int igd_vbios_find_saved_bdsm(const uint8_t *rom, size_t rom_size,
> > + uint16_t *bdsm_offset)
> > +{
> > + static const uint8_t start[] = { 0x66, 0x2e, 0x83, 0x3e };
> > + static const uint8_t middle[] = { 0x00, 0x74, 0x07, 0x66, 0x2e, 0xa1 };
> > + static const uint8_t end[] = { 0xeb, 0x0f, 0xb8, 0x5e, 0x10 };
> > + size_t i;
> > + bool found = false;
> > +
> > + if (rom_size < 19) {
> > + return -ENOENT;
> > + }
> > +
> > + for (i = 0; i + 19 <= rom_size; i++) {
> > + if (memcmp(rom + i, start, sizeof(start)) != 0 ||
> > + memcmp(rom + i + 6, middle, sizeof(middle)) != 0 ||
> > + memcmp(rom + i + 14, end, sizeof(end)) != 0) {
> > + continue;
> > + }
> > +
> > + /* same saved value address? */
> > + if (rom[i + 4] != rom[i + 12] || rom[i + 5] != rom[i + 13]) {
> > + continue;
> > + }
> > +
> > + if (found) {
> > + return -EEXIST;
> > + }
> > +
> > + *bdsm_offset = rom[i + 4] | ((uint16_t)rom[i + 5] << 8);
>
> This needs a bounds check to make sure it's still within the ROM,
> either here or before the memset().
>
> > + found = true;
> > + }
> > +
> > + if (!found) {
> > + return -ENOENT;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +void vfio_probe_igd_legacy_rom_quirk(VFIOPCIDevice *vdev)
> > +{
> > + int ret, gen;
> > + uint16_t pcir_offset, bdsm_offset = 0;
> > + uint8_t checksum;
> > +
> > + if (!vfio_pci_is(vdev, PCI_VENDOR_ID_INTEL, PCI_ANY_ID) ||
> > + !vfio_is_vga(vdev) || !vdev->vga) {
> > + return;
> > + }
> > +
> > + /* Only Gen 6~9 devices have legacy VBIOS as Option ROM */
> > + gen = igd_gen(vdev);
> > + if (gen < 6 || gen > 9) {
> > + return;
> > + }
> > +
> > + if (pci_get_word(vdev->rom) != 0xaa55) {
> > + return;
> > + }
> > +
> > + /* Must be a legacy ROM */
> > + pcir_offset = pci_get_word(vdev->rom + 0x18);
> > + if (pcir_offset >= vdev->rom_size ||
>
> This gates the below test to pcir_offset + 0x14, so should be:
>
> if (pcir_offset + 0x14 >= vdev->rom_size ||
>
> > + memcmp(vdev->rom + pcir_offset, "PCIR", 4) ||
> > + pci_get_byte(vdev->rom + pcir_offset + 0x14) != 0x00) {
> > + return;
> > + }
> > +
> > + ret = igd_vbios_find_saved_bdsm(vdev->rom, vdev->rom_size, &bdsm_offset);
> > + if (ret < 0) {
> > + return;
> > + }
> > +
> > + memset(vdev->rom + bdsm_offset, 0, sizeof(uint32_t));
> > +
> > + checksum = pci_rom_calculate_checksum(vdev->rom, vdev->rom_size);
> > + ((uint8_t *)vdev->rom)[6] = checksum;
>
> If updated per previous recommendation, this becomes:
>
> ((uint8_t *)vdev->rom)[6] -= checksum;
>
> Thanks,
> Alex
>
© 2016 - 2026 Red Hat, Inc.