Skip to content

Kernel use-after-free panic: mst_ioctl() dereferences stale struct pci_dev after PCIe hotplug removal (mstflint_access) #1786

Description

@ActiveInAI

Summary

On NVIDIA DGX Spark, running mstflint after a ConnectX-7 cable hotplug event reliably panics the kernel.

The mstflint_access kernel module caches a struct pci_dev * in struct mst_dev_data and never invalidates it. NVIDIA's own hotplug handler (dgx-spark-mlnx-hotplug) responds to a CX7 cable event by writing to /sys/bus/pci/devices/<bdf>/remove, which frees those struct pci_dev objects. The character devices (/dev/<bdf>_mstconf) survive the removal. The next MST_PARAMS ioctl dereferences the freed pointer → use-after-free → Oopspanic_on_oops=1 → unplanned reboot.

Both components involved are shipped by NVIDIA, and the module is force-loaded at boot on every DGX Spark, so every unit is exposed by default.

Impact

  • Kernel use-after-free → panic → unplanned reboot (data loss on a running appliance).
  • Triggered by a supported workflow: replug a CX7 cable, then query NIC firmware with mstflint.
  • Reachable as root only (/dev/*_mstconf is crw------- root:root), so this is a reliability / kernel-memory-safety bug, not a privilege boundary crossing. A read of freed slab memory is nonetheless undefined behavior and the faulting pointer is fully attacker-influenced by whatever reuses the slab.

Environment

Item Value
Platform NVIDIA NVIDIA_DGX_Spark/P4242 (aarch64)
BIOS 5.36_0ACUM018 08/06/2025
Kernel 6.17.0-1021-nvidia #21-Ubuntu (linux-image-nvidia-hwe-24.04 = 6.17.0-1021.21)
Faulting module mstflint_access 2.0.0, shipped inside linux-modules-6.17.0-1021-nvidia (not DKMS, not the Ubuntu mstflint deb)
Autoload nvidia-mstflint-loader 23.03-1/etc/modules-load.d/nvidia-mstflint.conf
Hotplug handler dgx-spark-mlnx-hotplug 26.01-1/lib/udev/rules.d/90-mtk-hotplug.rules/opt/nvidia/dgx-spark-mlnx-hotplug/mtk-hotplug-handler.sh
Hotplug driver cx7-pcie-hotplug (mtk_pcie_hotplug), ACPI dev MTKP0001:00
Userspace mstflint 4.26.0 (Git SHA 9f7f49c), deb 4.26.0+1-2ubuntu3
NIC 4× ConnectX-7 MT2910 [15b3:1021] @ 0000:01:00.{0,1}, 0002:01:00.{0,1}
NIC firmware 28.45.4028

Reproduction

Observed once in production (uptime 9 days). Not deliberately re-run — it panics the host. The sequence recovered from logs:

  1. Boot. mstflint_access is auto-loaded 8 s in; it creates /dev/0000:01:00.0_mstconf etc. and caches each struct pci_dev *.
  2. Unplug the ConnectX-7 cable.
    cx7-pcie-hotplug MTKP0001:00: Cable removal
    udev runs mtk-hotplug-handler.sh removal, whose remove_devices_in_domain() does:
    echo 1 > /sys/bus/pci/devices/${device_bdf}/remove
    → every struct pci_dev in the domain is freed. mstflint_access is not notified and its char devices remain.
  3. Replug the cable (13 s later).
    cx7-pcie-hotplug MTKP0001:00: Cable pluginpci_rescan_bus()mlx5_core re-probes, new pci_dev objects are allocated.
    (PCIe AER also logs Multiple Uncorrectable (Fatal) error message received on both root ports during this window.)
  4. ~4 minutes later, run mstflint -d <bdf> q.
    open("/dev/<bdf>_mstconf")MST_PARAMS ioctl → dereference of the stale pci_dev → Oops.

Root cause

Upstream kernel/mst_main.c (built as mstflint_access.ko):

  • struct mst_dev_data holds struct pci_dev *pci_dev;
  • mst_ioctl() case MST_PARAMS reads pci_dev->bus->number, pci_dev->devfn, pci_dev->device, …
  • There is no struct pci_driver with .remove(), no PCI bus notifier, and no pci_error_handlers. Nothing invalidates or refcounts the cached pointer when the device disappears.

The register state proves exactly this chain:

pc : mst_ioctl+0x960/0x1810 [mstflint_access]
Code: 97fffc4a f9402260 52800003 f9400801 (b940dc22)
                                           ^^^^^^^^ ldr w2, [x1, #0xdc]

x19 = ffff0000939b2c00   ; struct mst_dev_data *
x0  = ffff000081dae000   ; = [x19 + 0x40]  -> dev->pci_dev   (freed slab, still mapped)
x1  = 00000b6c000002e4   ; = [x0  + 0x10]  -> pci_dev->bus   (garbage: slab reused)
fault addr = 00000b6c000003c0  ; = x1 + 0xdc  -> bus->number

0x2e4 + 0xdc = 0x3c0 — the faulting address is exactly pci_dev->bus + 0xDC. Offset 0x10 into struct pci_dev is struct pci_bus *bus (immediately after the 16-byte bus_list). x0 still points into a valid slab page, but its contents have been overwritten by whatever reallocated the freed pci_dev — a textbook use-after-free read (WnR=0, FSC=0x04 level 0 translation fault).

Suggested fix

Any one of these closes it; (1)+(2) together is the clean fix:

  1. Invalidate on removal. Register a PCI bus notifier and, on BUS_NOTIFY_DEL_DEVICE / BUS_NOTIFY_REMOVED_DEVICE, take the device lock, set mst_dev_data->pci_dev = NULL, and have every mst_ioctl case return -ENODEV when it is NULL. (Deleting/hiding the char device at the same time would be better still.)
  2. Hold a reference. pci_dev_get() when binding the char device, pci_dev_put() on release, so the struct pci_dev cannot be freed while a handle is open. Note that a refcount alone is not sufficient — the device is still removed, and pci_dev->bus may be torn down — so this must be combined with (1).
  3. Become a real PCI driver with a .remove() callback instead of a bare chardev that squirrels away a pointer it does not own.

On the DGX Spark integration side, mtk-hotplug-handler.sh unbinds/removes PCI devices out from under a module that NVIDIA also force-loads at boot via /etc/modules-load.d/nvidia-mstflint.conf. Even with the module fixed, it is worth making the hotplug handler aware of consumers of those devices.

Workaround until fixed: after any CX7 cable hotplug event, do not run mstflint / mstconfig / anything opening /dev/*_mstconf until the host has been rebooted (or rmmod mstflint_access first, then reload).

Full Oops

Unable to handle kernel paging request at virtual address 00000b6c000003c0
Mem abort info:
  ESR = 0x0000000096000004
  EC = 0x25: DABT (current EL), IL = 32 bits
  SET = 0, FnV = 0
  EA = 0, S1PTW = 0
  FSC = 0x04: level 0 translation fault
Data abort info:
  ISV = 0, ISS = 0x00000004, ISS2 = 0x00000000
  CM = 0, WnR = 0, TnD = 0, TagAccess = 0
  GCS = 0, Overlay = 0, DirtyBit = 0, Xs = 0
user pgtable: 4k pages, 48-bit VAs, pgdp=000000080cf97000
[00000b6c000003c0] pgd=0000000000000000, p4d=0000000000000000
Internal error: Oops: 0000000096000004 [#1]  SMP
CPU: 13 UID: 0 PID: 1768087 Comm: mstflint Tainted: G        W  O        6.17.0-1021-nvidia #21-Ubuntu PREEMPT(none)
Tainted: [W]=WARN, [O]=OOT_MODULE
Hardware name: NVIDIA NVIDIA_DGX_Spark/P4242, BIOS 5.36_0ACUM018 08/06/2025
pstate: 63400009 (nZCv daif +PAN -UAO +TCO +DIT -SSBS BTYPE=--)
pc : mst_ioctl+0x960/0x1810 [mstflint_access]
lr : mst_ioctl+0x954/0x1810 [mstflint_access]
sp : ffff8000c65db8e0
x29: ffff8000c65dbb50 x28: ffffbc05cebe50a0 x27: 0000000000000000
x26: 0000000000000000 x25: 0000000000000000 x24: ffff000096a57808
x23: c8e5bc05cebe2de8 x22: 000000008028d001 x21: ffff8000c65db910
x20: 0000ffffcaceb428 x19: ffff0000939b2c00 x18: ffff80009fdc0040
x17: 0000000000000000 x16: 0000000000000000 x15: 0000000000000000
x14: 0000000000000000 x13: 0000000000000000 x12: 0000000000000000
x11: 0000000000000000 x10: 0000000000000000 x9 : ffffbc05cebe1efc
x8 : 0000000000000000 x7 : 0000000000000000 x6 : 0000000000000000
x5 : 0000000000000000 x4 : 0000000000000000 x3 : 0000000000000000
x2 : 0000000000000000 x1 : 00000b6c000002e4 x0 : ffff000081dae000
Call trace:
 mst_ioctl+0x960/0x1810 [mstflint_access] (P)
 unlocked_ioctl+0x30/0x60 [mstflint_access]
 __arm64_sys_ioctl+0xd0/0x160
 invoke_syscall+0x74/0x128
 el0_svc_common.constprop.0+0x4c/0x140
 do_el0_svc+0x28/0x58
 el0_svc+0x40/0x198
 el0t_64_sync_handler+0xc0/0x108
 el0t_64_sync+0x1b8/0x1c0
Code: 97fffc4a f9402260 52800003 f9400801 (b940dc22)
---[ end trace 0000000000000000 ]---

Preceding hotplug window (same boot, 4 minutes earlier)

cx7-pcie-hotplug MTKP0001:00: Cable removal            (T-4m25s)
cx7-pcie-hotplug MTKP0001:00: Cable plugin             (T-4m12s)
pcieport 0000:00:00.0: AER: Multiple Uncorrectable (Fatal) error message received from 0000:ff:1f.7
pcieport 0002:00:00.0: AER: Multiple Uncorrectable (Fatal) error message received from 0002:ff:1f.7
mlx5_core 0000:01:00.0: enabling device (0000 -> 0002)   ... (re-probe of all 4 functions)

A secondary, non-fatal issue in the same window (kernel tainted W, likely unrelated to the panic but worth a look):

mtk-hotplug-han: page allocation failure: order:8, mode:0x40dc0(GFP_KERNEL|__GFP_ZERO|__GFP_COMP)
Call trace:
 ...
 mlx5_fw_tracer_allocate_strings_db+0x50/0x160 [mlx5_core]
 mlx5_fw_tracer_create+0x1fc/0x368 [mlx5_core]
 mlx5_init_once+0x170/0x540 [mlx5_core]
 probe_one+0x104/0x248 [mlx5_core]
 ...
 pci_rescan_bus+0x3c/0x68
 dev_rescan_store+0xac/0xc8

mlx5_fw_tracer_create() requests an order-8 (1 MB) contiguous allocation during hotplug re-probe and does not abort probe when it fails. The host had 26 GB free at the time — this is high-order fragmentation, not memory exhaustion.

Notes on evidence

kdump is non-functional on the stock DGX Spark image (crashkernel=1G-:0M reserves 0 MB; USE_KDUMP unset in /etc/default/kdump-tools), so no vmcore exists. The Oops above was recovered from efi_pstore via systemd-pstore's archive at /var/lib/systemd/pstore/<epoch>/001/dmesg.txt. Suggest fixing the shipped crashkernel= value.

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