[kvm-unit-tests PATCH v4] x86: pci: Support unaligned register access in PCI config read/write

Irene Wang <[email protected]>
Newsgroups org.kernel.vger.kvm
Message-ID <[email protected]>
Per the PCI Local Bus Specification (Section 3.2.2.3.2, "Configuration
Mechanism #1"), port 0xCF8 (CONFIG_ADDRESS) requires a DWORD-aligned
register offset (bits [1:0] = 00b), while byte and word offsets within
the DWORD must be selected via data port 0xCFC + (reg & 3).

Previously, PCI_CONF1_ADDRESS did not clear bits [1:0], and 8-bit /
16-bit helpers always accessed base port 0xCFC directly. This worked
in QEMU because its PCI host bridge emulation preserves unaligned bits
in CONFIG_ADDRESS and uses them during CONFIG_DATA accesses. However,
in strictly spec-compliant VMMs (and potentially real hardware), bits
[1:0] of 0xCF8 are ignored, causing non-aligned reads/writes at port
0xCFC to erroneously target byte 0 of the DWORD. In practice, this
causes pci_find_dev() to read the vendor ID twice instead of reading
the vendor ID and device ID.

Fix this by masking `reg` with `~3` in PCI_CONF1_ADDRESS and adding
the `(reg & 3)` offset to the CONFIG_DATA port for 8-bit and 16-bit
accessors, ensuring compatibility across QEMU and other VMMs.
Additionally, add an ASSERT_PCI_CONF1_VALID macro to guard against
illegal accesses that cross DWORD boundaries.

Assisted-by: Gemini:gemini-3.6-flash
Reviewed-by: Jim Mattson <[email protected]>
Reviewed-by: David Matlack <[email protected]>
Signed-off-by: Irene Wang <[email protected]>
---
v3 -> v4:
 - Address comment formatting nits (David)

v2 -> v3:
 - Add comment in PCI_CONF1_ADDRESS explicitly explaining that clearing
   bits [1:0] prevents QEMU from double-offsetting accesses (David)
 - Introduce ASSERT_PCI_CONF1_VALID macro checking that access offset
   + access size do not read/write past 4-byte window (David)

v1 -> v2:
 - Enclose macro parameter `dev` in parentheses in PCI_CONF1_ADDRESS
   (Jim)
 - Add assert((reg & 3) != 3) in pci_config_readw() and
   pci_config_writew() to guard against illegal 16-bit accesses
   crossing DWORD boundaries (Jim)

Link to v3: https://lore.kernel.org/kvm/[email protected]/
Link to v2: https://lore.kernel.org/kvm/[email protected]/
Link to v1: https://lore.kernel.org/kvm/[email protected]/
---
 lib/x86/asm/pci.h | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/lib/x86/asm/pci.h b/lib/x86/asm/pci.h
index 03e55c27..622d7fd4 100644
--- a/lib/x86/asm/pci.h
+++ b/lib/x86/asm/pci.h
@@ -9,22 +9,33 @@
 #include "pci.h"
 #include "x86/asm/io.h"
 
-#define PCI_CONF1_ADDRESS(dev, reg)	((0x1 << 31) | (dev << 8) | reg)
+/*
+ * Bits [1:0] offset into data port, not address port. Spec compliant host
+ * bridges ignore them in CONFIG_ADDRESS, but QEMU does not; mask them out here
+ * so QEMU doesn't double-offset the access.
+ */
+#define PCI_CONF1_ADDRESS(dev, reg)	((0x1 << 31) | ((dev) << 8) | ((reg) & ~3))
+
+/* Ensure access offset + size stays within the 4-byte (DWORD) boundary. */
+#define ASSERT_PCI_CONF1_VALID(reg, type) \
+	assert(((reg) & 3) + sizeof(type) <= 4)
 
 static inline uint8_t pci_config_readb(pcidevaddr_t dev, uint8_t reg)
 {
     outl(PCI_CONF1_ADDRESS(dev, reg), 0xCF8);
-    return inb(0xCFC);
+    return inb(0xCFC + (reg & 3));
 }
 
 static inline uint16_t pci_config_readw(pcidevaddr_t dev, uint8_t reg)
 {
+    ASSERT_PCI_CONF1_VALID(reg, uint16_t);
     outl(PCI_CONF1_ADDRESS(dev, reg), 0xCF8);
-    return inw(0xCFC);
+    return inw(0xCFC + (reg & 3));
 }
 
 static inline uint32_t pci_config_readl(pcidevaddr_t dev, uint8_t reg)
 {
+    ASSERT_PCI_CONF1_VALID(reg, uint32_t);
     outl(PCI_CONF1_ADDRESS(dev, reg), 0xCF8);
     return inl(0xCFC);
 }
@@ -33,19 +44,21 @@ static inline void pci_config_writeb(pcidevaddr_t dev, uint8_t reg,
                                      uint8_t val)
 {
     outl(PCI_CONF1_ADDRESS(dev, reg), 0xCF8);
-    outb(val, 0xCFC);
+    outb(val, 0xCFC + (reg & 3));
 }
 
 static inline void pci_config_writew(pcidevaddr_t dev, uint8_t reg,
                                      uint16_t val)
 {
+    ASSERT_PCI_CONF1_VALID(reg, uint16_t);
     outl(PCI_CONF1_ADDRESS(dev, reg), 0xCF8);
-    outw(val, 0xCFC);
+    outw(val, 0xCFC + (reg & 3));
 }
 
 static inline void pci_config_writel(pcidevaddr_t dev, uint8_t reg,
                                      uint32_t val)
 {
+    ASSERT_PCI_CONF1_VALID(reg, uint32_t);
     outl(PCI_CONF1_ADDRESS(dev, reg), 0xCF8);
     outl(val, 0xCFC);
 }
-- 
2.55.0.699.gb54405d56f-goog
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.