Skip to content

[Bug] pci: unvalidated device-controlled MSI-X table BIR/offset in msix_table_remap() gives arbitrary physical memory write at boot #11985

Closed

Description

@XueDugu
  • RT-Thread Version: master (v5.4.0-dev, commit 5d1fb66, 2026-10-15); affected since v5.2.0
    (introduced by 2168ed8, 2024-05-30, "[DM/Feature] Basic PCI/PCIe bus")
  • Affected area: Device drivers → components/drivers/pci/ (MSI/MSI-X core, msi/msi.c)
  • Architecture: ARM64 (any arch with RT_PCI_MSI; verified on aarch64)
  • Board / hardware: QEMU qemu-virt64-aarch64 official default .config
    (RT_USING_PCI=y, RT_PCI_MSI=y, RT_PCI_ECAM=y, RT_NVME_PCI=y, RT_PIC_ARM_GIC_V2M=y),
    stock nvme PCIe device. Any board where a driver enables MSI-X at probe (nvme, ivshmem, virtio-pci) is affected.
  • Develop Toolchain: arm-gnu-toolchain-13.2.Rel1 + scons; QEMU 9.2.3 (device-side injection via local
    QEMU nvme model patch to forge the read-only capability register)

Describe the bug

msix_table_remap() (components/drivers/pci/msi/msi.c:737) reads the device-controlled,
read-only
MSI-X Table Offset register (PCIR_MSIX_TABLE, capability offset +4) and uses it without
any validation:

rt_pci_read_config_u32(pdev, pdev->msix_cap + PCIR_MSIX_TABLE, &table_offset);
bir = (rt_uint8_t)(table_offset & PCIM_MSIX_BIR_MASK);      /* 0x7 -> bir = 0..7 */

if (pdev->resource[bir].flags & PCI_BUS_REGION_F_NONE) { ... return RT_NULL; }

table_base_phys = pdev->resource[bir].base + (table_offset & ~PCIM_MSIX_BIR_MASK);
return rt_ioremap((void *)table_base_phys, entries_nr * PCIM_MSIX_ENTRY_SIZE);

Three validations required by the spec / carried by Linux are missing:

  1. BIR range: PCI spec allows BIR = 0–5, but bir here is masked with 0x7 (0–7) and used directly
    as an index into pdev->resource[RT_PCI_BAR_NR_MAX = 6]. With bir = 6 the access aliases the
    adjacent member pdev->rom (include/drivers/pci.h:161-162), which is {base:0, size:0, flags:0}
    for a device without an option ROM. Since PCI_BUS_REGION_F_MEM == 0, the "invalid BAR" guard
    (flags & PCI_BUS_REGION_F_NONE) passes, and table_base_phys = 0 + (table_offset & ~7) becomes an
    absolute, fully attacker-chosen physical address (29 bits — covers all RAM on the reference board).
  2. Table-within-BAR bound: even for bir = 0..5 pointing at a real BAR, table_offset (up to
    0xFFFFFFF8) is never checked against resource[bir].size.
  3. Table size consistency: entries_nr comes from a separate read of PCIR_MSIX_CTRL
    (msix_capability_init), so a device returning inconsistent reads makes the final masking loop
    (0x80000000 per entry, up to 2048 entries = 32 KB) run past the remapped region.

After the bogus rt_ioremap() succeeds (it performs no physical-address validation,
components/mm/ioremap.c:92), MSI-X initialization reads and writes through the mapping:

  • msix_setup_msi_descs() reads entry+0xc (kernel-memory disclosure path);
  • rt_pci_msi_write_msg() writes msg.address_lo (GIC MSI doorbell, e.g. 0x08020040),
    msg.address_hi (0) and msg.data (SPI number) at entry+0/4/8;
  • the final loop sprays 0x80000000 over every entry's vector_ctrl.

To reproduce (full chain, official image, zero interaction)

Reference: rtthread-ws/audit/RT_pci/exp/ (patch, scripts, GDB and console logs).

  1. Build stock bsp/qemu-virt64-aarch64 (official default config) — no source change needed.
  2. Control: boot QEMU (-M virt,its=on,gic-version=2) with a stock nvme device → boots to msh,
    nvme0n1 registers, list_blk works (control-stock.log). GDB shows the exact chain runs at boot:
    pci_nvme_probe → rt_pci_msix_enable → msix_capability_init → msix_table_remap
    (gdb-msix-hit.log).
  3. Attack: same command, with the device's read-only MSI-X Table Offset register forged to
    0x4023f06e (= phys &rt_interrupt_enter_hook 0x4023f068 | bir 6) — done with a small local QEMU
    nvme-model patch (qemu-nvme-msix-inject.patch, emulating a device whose firmware reports
    ill-formatted MSI-X metadata; the real MSI-X table MMIO stays inside BAR0).
  4. Observed (attack2-gdb.log, attack2-console.log):
    • GDB watchpoint on the remapped entry: kernel writes 0x08020040 (MSI doorbell) into physical
      0x4023f068; backtrace rt_pci_msi_write_msg ← rt_pci_msi_setup_irqs ← msix_capability_init ← rt_pci_msix_enable ← pci_nvme_probe;
    • physical memory before: all zeros, after: MSI message fields;
    • on the next interrupt the corrupted hook is called:
      exception info: esr.EC:0x21, epc:0x0000000008020040, Instruction abort from current exception level
      in an endless exception loop — control flow hijacked to the value the device planted.

Expected behaviour

bir must be range-checked (bir >= RT_PCI_BAR_NR_MAX → reject) and table_offset + entries_nr * 16 must be validated against resource[bir].size before rt_ioremap()
(also reject pdev->rom/unassigned BARs and size == 0).

Actual behaviour

The kernel maps and reads/writes an arbitrary attacker-chosen physical address during boot-time driver
probe, corrupting kernel memory (function-pointer hijack demonstrated; EPC equals the planted value),
then dead-loops in the exception handler.

Other additional context

  • Attack prerequisite (explicit): a malicious or compromised PCIe device (e.g. NVMe SSD with
    malicious firmware, supply-chain-tampered peripheral, or forged device on an exposed M.2/PCIe slot)
    that reports an ill-formatted MSI-X capability. No software privilege or user interaction is needed;
    the vulnerable path runs automatically during boot on the official qemu-virt64-aarch64 default config.
  • Severity: CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 6.8 — the score is capped by the
    physical/peripheral vector, but the technical impact is arbitrary kernel physical-memory read/write
    at boot, i.e. full kernel compromise from a malicious device.
  • CWE: CWE-787 (out-of-bounds write) / CWE-129 (improper validation of array index) /
    CWE-20 (improper input validation of device-controlled field).
  • Versions: introduced by 2168ed8 (2024-05-30); first affected release v5.2.0; v5.2.0–v5.3.0
    and master (2026-11-05) verified affected; no fix or existing report found on GitHub.
  • Relation to existing reports: new — not related to any NVD CVE or existing issue (the RT-Thread
    PCI framework has had no security audit coverage; [Bug] nvme: kernel heap memory leak per inactive namespace in nvme_scan_device() exhausts system heap and can brick boot #11919 concerns nvme namespace scanning, a
    different root cause).

Activity

  1. github-actions commented on Nov 4, 2026

    @github-actions

    如需认领并处理此 Issue,请评论 /claim。

    To claim and work on this issue, comment /claim.

  2. BernardXiong commented on Nov 4, 2026

    @BernardXiong
    Member

    已确认,这个问题是真实存在的,感谢报告。

    我核对了代码:msix_table_remap() 里的 bir = table_offset & 0x7 会得到 0~7,但 pdev->resource[] 只有 RT_PCI_BAR_NR_MAX(6)项。bir = 6 时索引会落到紧随其后的 pdev->rom,无 option ROM 的设备该成员是全 0,flags & PCI_BUS_REGION_F_NONE 的判空正好被绕过,于是 base + offset 变成设备可完全控制的物理地址;叠加 table offset / 长度从未和 BAR size 比较,rt_ioremap() 之后就会对任意物理地址读写,报告里的分析链路成立。bir = 7 则是直接的数组越界。

    修复已提交 PR #11986,主要做了四件事:

    1. 拒绝 bir >= RT_ARRAY_SIZE(pdev->resource);
    2. 拒绝未分配(PCI_BUS_REGION_F_NONE)或 size 为 0 的 BAR;
    3. 校验 table_offset + entries_nr * 16 必须完整落在 BAR 内;
    4. 检查配置空间读取结果,并拒绝 nvec > table_size,避免设备多次读取返回不一致的值。

    另外补充一点:当前 master 的 qemu-virt64-aarch64 默认 .config 在 c89121a 之后已关闭 PCI,所以默认配置本身不会走到这条路径;但只要启用 RT_USING_PCI / RT_PCI_MSI(包括报告验证过的版本),问题就存在,修复仍然必要。

    已在本地用 xPack aarch64-none-elf-gcc 13.2.1 打开 PCI/MSI 编译 qemu-virt64-aarch64,编译通过,CI 结果会继续跟进。


    Confirmed — this is a real issue. Thanks for the detailed report.

    The analysis holds: bir = table_offset & 0x7 yields 0..7 while pdev->resource[] has only 6 entries. With bir = 6 the index aliases pdev->rom, which is all-zero when there is no option ROM, so the PCI_BUS_REGION_F_NONE check is bypassed and base + offset becomes a fully device-controlled physical address. The table offset and length are also never checked against the BAR size, so the following rt_ioremap() gives read/write access to that address. bir = 7 is an out-of-bounds array access as well.

    Fix is in PR #11986:

    1. reject bir >= RT_ARRAY_SIZE(pdev->resource);
    2. reject unassigned (PCI_BUS_REGION_F_NONE) or zero-sized BARs;
    3. validate that table_offset + entries_nr * 16 is fully contained in the BAR;
    4. check the config read result and reject nvec > table_size, so inconsistent device reads cannot exceed the mapped table.

    One note: on current master the default qemu-virt64-aarch64 .config disables PCI (since c89121a), so the default config no longer reaches this path. The bug is still reachable whenever RT_USING_PCI / RT_PCI_MSI is enabled, including the versions verified in the report.

    Verified locally by building qemu-virt64-aarch64 with PCI/MSI enabled using xPack aarch64-none-elf-gcc 13.2.1. I will keep an eye on CI.

  3. added a commit that references this issue on Nov 5, 2026
    f6857f9
  4. added a commit that references this issue on Nov 13, 2026
    cfed5da
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions