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:
-
ctlr->devis the PCI controller device (ata-pci.c:207:ctlr->dev = dev). The PCI bus setdev->ivars = &pci_devinfoatpci.c:3110(device_set_ivars(dinfo->cfg.dev, dinfo)).device_set_ivars(ctlr->dev, serial)atata-acard.c:221unconditionally overwrites that pointer. After this,pci_read_config_method(pci.c:4139-4146) doesdinfo = device_get_ivars(child); cfg = &dinfo->cfg;βdinfonow points tostruct ata_serialize(40 bytes).cfg = dinfo + offsetof(pci_devinfo, cfg) = serial+16.cfg->busis atserial+16+68 = serial+84,cfg->slotatserial+85,cfg->funcatserial+86β all 44-46 bytes past the 40-bytekmalloc. This is a deterministic heap OOB read producing 3 bytes of garbage used as the PCI b/d/f address. -
static int inited(line 212) is function-local static β shared across ALL controller instances and ALL channels. The firstata_serializecall ever (duringata_attach β ATA_LOCKING,ata-all.c:128) sets it and clobbers controller-1's ivars. A second ATP850R controller's first call seesinited==1, takes theelsebranch (line 225), and callsdevice_get_ivars(ctlr2->dev)β butctlr2->devwas NEVERdevice_set_ivars'd byata_serialize, so it still holds the originalpci_devinfo*. Thatpci_devinfo*is reinterpreted asstruct ata_serialize*, andlockmgr(&serial->locked_mtx, ...)at line 227 operates on the PCISTAILQnext-pointer /resource_listhead fields ofpci_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_serializeallocation β undefined behavior, potential info disclosure of adjacent slab contents. - (b)
pci_write_configwith garbage b/d/f may hit a real PCI device and overwrite its register0x54with 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_devinfoasstruct ata_serializeandlockmgrcorrupts 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, device0x0005) 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_serializeis called duringata_attach(ata-all.c:128) and clobbersctlr->dev->ivarsbefore 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
- Boot DragonFlyBSD on hardware containing an ATP850R + disk.
- During boot, nata auto-attaches.
ata_acard_chipinitsetsctlr->locking = ata_serialize(ata-acard.c:76). ata_attach(channel)callsATA_LOCKING(dev, ATA_LF_LOCK)(ata-all.c:128) βata_serializeβdevice_set_ivars(ctlr->dev, serial)clobberspci_devinfo.- Disk identify triggers
ata_acard_850_setmodeβpci_read_config(gparent, 0x54, 1)(line 146).pci_read_config_methodreadscfg->bus/slot/funcfromserial+84..86(heap OOB). - Observe:
bootverbosedmesg showsFAILURE setting UDMA33 on ATP850 chipbecauseata_controlcmd'sSETXFERsucceeds 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 register0x54is corrupted β observable as a malfunction or kernel panic on an unrelated device.
Two-controller PoC (type confusion β panic)
- Install two ATP850R PCI cards.
- Boot. Controller-1's
ata_serializesetsinited=1, clobbers ctlr1 ivars. - Controller-2's channel attach calls
ata_serializeβinited==1βserial = device_get_ivars(ctlr2->dev)=pci_devinfo*(never clobbered). lockmgr(&serial->locked_mtx, LK_EXCLUSIVE)(line 227) interpretspci_devinfo.pci_links.stqe_nextasstruct lock, writing lock state into the PCI device linked list.- Next PCI bus scan or config access corrupts/panics β typically a NULL
deref or assertion in
subr_bus.cwhen 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).
Recommended fix
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
sys/bus/pci/pci.c:3110βdevice_set_ivars(dinfo->cfg.dev, dinfo)sets the PCI device ivars topci_devinfo*.sys/bus/pci/pci.c:4139-4146βpci_read_config_methoddereferencesdevice_get_ivars(child)aspci_devinfo*.sys/dev/disk/nata/ata-pci.c:207βctlr->dev = dev(the PCI controller device).sys/dev/disk/nata/ata-all.c:128βATA_LOCKINGcaller that invokesata_serialize.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2114 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedbatch build rc=0
batch 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
No comments yet.