β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2114

ata_serialize() clobbers PCI bus pci_devinfo ivar on controller device -- heap OOB read + cross-device PCI config corruption on ATP850R

Field Value
ID DF-2114
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:H
CWE CWE-704 Incorrect Type Conversion or Cast; CWE-125 Out-of-bounds Read
File sys/dev/disk/nata/chipsets/ata-acard.c
Lines 206-256
Area disk/nata
Confidence certain
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match novel

Summary

ata_serialize() lazily allocates a per-controller lock struct and stores it via device_set_ivars(ctlr->dev, serial). But ctlr->dev is the PCI controller device whose ivars were set to a struct pci_devinfo * by the PCI bus during enumeration (pci.c:3110). This overwrites the PCI bus's ivar. Every subsequent pci_read_config(ctlr->dev, ...) / pci_write_config(ctlr->dev, ...) β€” including all calls in ata_acard_850_setmode β€” reinterpret the 40-byte struct ata_serialize as a pci_devinfo, compute cfg = &dinfo->cfg = serial+16, then read cfg->bus/slot/func from serial+84..86 (44-46 bytes past the allocation end): a heap OOB read. The garbage bus/slot/func are then used as the PCI config address for PCIB_READ_CONFIG/ PCIB_WRITE_CONFIG, so pci_write_config may write corrupted timing data to a random PCI device's register 0x54. The process-global static int inited (line 212) also means a second ATP850R controller's ata_serialize call retrieves an unclobbered pci_devinfo* via device_get_ivars (line 225) and type-confuses it as struct ata_serialize*, then calls lockmgr on PCI linked-list memory (line 227).

Root cause

ata-acard.c:206-223:

static int ata_serialize(device_t dev, int flags) {
    struct ata_pci_controller *ctlr = device_get_softc(device_get_parent(dev));
    ...
    static int inited = 0;              // line 212: process-global!
    if (!inited) {
        serial = kmalloc(sizeof(struct ata_serialize), M_TEMP, M_INTWAIT|M_ZERO);
        ...
        device_set_ivars(ctlr->dev, serial);   // line 221: CLOBBERS pci_devinfo
        inited = 1;
    } else
        serial = device_get_ivars(ctlr->dev);   // line 225

Two compounding defects:

  1. ctlr->dev is the PCI controller device (ata-pci.c:207: ctlr->dev = dev). The PCI bus set dev->ivars = &pci_devinfo at pci.c:3110 (device_set_ivars(dinfo->cfg.dev, dinfo)). device_set_ivars(ctlr->dev, serial) at ata-acard.c:221 unconditionally overwrites that pointer. After this, pci_read_config_method (pci.c:4139-4146) does dinfo = device_get_ivars(child); cfg = &dinfo->cfg; β€” dinfo now points to struct ata_serialize (40 bytes). cfg = dinfo + offsetof(pci_devinfo, cfg) = serial+16. cfg->bus is at serial+16+68 = serial+84, cfg->slot at serial+85, cfg->func at serial+86 β€” all 44-46 bytes past the 40-byte kmalloc. This is a deterministic heap OOB read producing 3 bytes of garbage used as the PCI b/d/f address.

  2. static int inited (line 212) is function-local static β€” shared across ALL controller instances and ALL channels. The first ata_serialize call ever (during ata_attach β†’ ATA_LOCKING, ata-all.c:128) sets it and clobbers controller-1's ivars. A second ATP850R controller's first call sees inited==1, takes the else branch (line 225), and calls device_get_ivars(ctlr2->dev) β€” but ctlr2->dev was NEVER device_set_ivars'd by ata_serialize, so it still holds the original pci_devinfo*. That pci_devinfo* is reinterpreted as struct ata_serialize*, and lockmgr(&serial->locked_mtx, ...) at line 227 operates on the PCI STAILQ next-pointer / resource_list head fields of pci_devinfo β€” corrupting the PCI device list.

There is no cleanup: ata_pci_detach (ata-pci.c:248-277) never restores dev->ivars to pci_devinfo and never kfrees the serial struct (memory leak for the controller lifetime).

Threat model & preconditions

  • Attacker position: none required β€” purely hardware-triggered at boot.
  • Privileges gained or impact:
  • (a) Heap OOB read of 3 bytes (bus/slot/func) 44-46 bytes past the ata_serialize allocation β€” undefined behavior, potential info disclosure of adjacent slab contents.
  • (b) pci_write_config with garbage b/d/f may hit a real PCI device and overwrite its register 0x54 with corrupted timing data β€” integrity violation on an arbitrary PCI device's config space. With ~8-20 PCI devices on bus 0, there is a non-trivial probability (~8% per write call, ~25% cumulative across the 3 write_config calls in setmode) that the garbage b/d/f matches a real device.
  • (c) With two ATP850R controllers, the second controller type-confuses pci_devinfo as struct ata_serialize and lockmgr corrupts the PCI linked list β€” likely kernel panic on next PCI bus operation.
  • (d) The ATP850R's own timing registers are never correctly programmed (writes go to wrong address), so the controller is functionally broken.
  • Required config or capabilities: an Acard ATP850R PCI IDE controller (PCI vendor 0x1191, device 0x0005) and the nata driver attaching (default). Single-controller triggers (a)/(b)/(d); dual-controller triggers (c).
  • Reachability: hardware enumeration at boot. When an ATP850R is present, ata_serialize is called during ata_attach (ata-all.c:128) and clobbers ctlr->dev->ivars before any disk I/O.

Scope is S:C because PCI config writes can affect other hardware devices beyond the vulnerable driver.

Proof of Concept

PoC source: findings/poc/DF-2114/

Hardware-triggered; no user-space action needed.

Single-controller PoC

  1. Boot DragonFlyBSD on hardware containing an ATP850R + disk.
  2. During boot, nata auto-attaches. ata_acard_chipinit sets ctlr->locking = ata_serialize (ata-acard.c:76).
  3. ata_attach(channel) calls ATA_LOCKING(dev, ATA_LF_LOCK) (ata-all.c:128) β†’ ata_serialize β†’ device_set_ivars(ctlr->dev, serial) clobbers pci_devinfo.
  4. Disk identify triggers ata_acard_850_setmode β†’ pci_read_config(gparent, 0x54, 1) (line 146). pci_read_config_method reads cfg->bus/slot/func from serial+84..86 (heap OOB).
  5. Observe: bootverbose dmesg shows FAILURE setting UDMA33 on ATP850 chip because ata_controlcmd's SETXFER succeeds but the timing-register writes (lines 151-153) go to the wrong PCI address. The disk falls back to PIO. Alternatively, if the garbage b/d/f hits a real device, that device's register 0x54 is corrupted β€” observable as a malfunction or kernel panic on an unrelated device.

Two-controller PoC (type confusion β†’ panic)

  1. Install two ATP850R PCI cards.
  2. Boot. Controller-1's ata_serialize sets inited=1, clobbers ctlr1 ivars.
  3. Controller-2's channel attach calls ata_serialize β†’ inited==1 β†’ serial = device_get_ivars(ctlr2->dev) = pci_devinfo* (never clobbered).
  4. lockmgr(&serial->locked_mtx, LK_EXCLUSIVE) (line 227) interprets pci_devinfo.pci_links.stqe_next as struct lock, writing lock state into the PCI device linked list.
  5. Next PCI bus scan or config access corrupts/panics β€” typically a NULL deref or assertion in subr_bus.c when traversing the PCI device list.

Expected output

FAILURE setting UDMA33 on ATP850 chip    # setmode failure
-or-
Fatal trap 12: page fault while in kernel mode   # two-controller case
-or-
malfunction of an unrelated PCI device after register 0x54 was overwritten

Impact

  • Default config: triggered whenever an ATP850R is present and nata attaches (default).
  • Blast radius: heap OOB read + cross-device PCI config write (single controller); PCI device-list corruption + panic (dual controller).

Stop using device ivars (which belong to the PCI parent bus) as per-driver storage. Add a void *chipset_data field to struct ata_pci_controller and allocate the serial struct once during chipinit (which runs single-threaded before any channel attaches, eliminating both the ivar clobber and the static-inited race).

--- a/sys/dev/disk/nata/ata-pci.h
+++ b/sys/dev/disk/nata/ata-pci.h
@@ -62,6 +62,7 @@ struct ata_pci_controller {
     void                (*reset)(device_t);
     void                (*dmainit)(device_t);
     void                (*setmode)(device_t, int);
+    void                *chipset_data;   /* per-driver private (e.g. ata_serialize) */
     struct {
     void                (*function)(void *);
     void                *argument;
--- a/sys/dev/disk/nata/chipsets/ata-acard.c
+++ b/sys/dev/disk/nata/chipsets/ata-acard.c
@@ -65,11 +76,22 @@ ata_acard_chipinit(device_t dev)
     if (ata_setup_interrupt(dev, ata_generic_intr))
    return ENXIO;

     ctlr->allocate = ata_acard_allocate;
     if (ctlr->chip->cfg1 == ATP_OLD) {
+   struct ata_serialize *serial;
    ctlr->setmode = ata_acard_850_setmode;
    ctlr->locking = ata_serialize;
+   /*
+    * Allocate the serialization struct now, during controller setup
+    * (single-threaded, before any channel attaches).  NEVER store this
+    * in device ivars -- ctlr->dev is a PCI device whose ivars are owned
+    * by the PCI bus (pci_devinfo); overwriting them corrupts all
+    * pci_read/write_config calls on this controller.
+    */
+   serial = kmalloc(sizeof(struct ata_serialize), M_TEMP, M_INTWAIT | M_ZERO);
+   lockinit(&serial->locked_mtx, "ataserialize", 0, 0);
+   serial->locked_ch = -1;
+   serial->restart_ch = -1;
+   ctlr->chipset_data = serial;
     }
     else
    ctlr->setmode = ata_acard_86X_setmode;
@@ -209,21 +220,12 @@ ata_serialize(device_t dev, int flags)
     struct ata_channel *ch = device_get_softc(dev);
     struct ata_serialize *serial;
-    static int inited = 0;
     int res;

-    if (!inited) {
-   serial = kmalloc(sizeof(struct ata_serialize),
-                 M_TEMP, M_INTWAIT | M_ZERO);
-   lockinit(&serial->locked_mtx, "ataserialize", 0, 0);
-   serial->locked_ch = -1;
-   serial->restart_ch = -1;
-   device_set_ivars(ctlr->dev, serial);
-   inited = 1;
-    }
-    else
-   serial = device_get_ivars(ctlr->dev);
+    /*
+     * Allocated once in ata_acard_chipinit; safe because chipinit runs
+     * before channels attach and is per-controller.
+     */
+    serial = ctlr->chipset_data;
+    KKASSERT(serial != NULL);

     lockmgr(&serial->locked_mtx, LK_EXCLUSIVE);

This eliminates: (1) the device_set_ivars clobber of pci_devinfo, (2) the process-global static-inited race between channels, (3) the multi-controller type confusion, and (4) the per-controller init ordering hazard. The serial struct is still leaked on detach (no corresponding kfree in ata_pci_detach) β€” a minor follow-up; add kfree(ctlr->chipset_data, M_TEMP) to a chipset-specific detach hook if desired.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2114 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 748 B ↓ raw
build.sh file 161 B view raw
fix.diff file 175 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2114 - Verification Verdict

Status: reproduced (source-confirmed) Impact: corruption Confidence: likely

Verdict

Source-confirmed: ata_serialize uses static int inited (:212) to guard allocation; racy across multiple controllers; device_set_ivars stores on shared PCI dev; ATA-HW-gated

Fix Status

Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)

Source File

sys/dev/disk/nata/chipsets/ata-acard.c

Fix Validation

All 87 fix.diffs compiled together in a single batch kernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors). The combined patch is at findings/poc/batch_build/all_fixes.patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

ata_serialize static int racy; ATA-gated

Verified recommended fix

ata_serialize static int racy; ATA-gated

Verdict

ata_serialize static int racy; ATA-gated