ata-pci: out-of-bounds interrupt[] write/read when AHCI reports >8 channels -> kernel heap corruption + RIP control on next IRQ
Summary
struct ata_pci_controller::interrupt[] fixed array of 8 (ata-pci.h:67 XXX SOS max ch#). ata-ahci.c:101-103 sets ctlr->channels up to 32 from MAX(flsl(ATA_AHCI_PI), (ATA_AHCI_CAP & ATA_AHCI_NPMASK)+1). ata-pci.c:230 loops unit < ctlr->channels adding up to 32 children. Each child->ata_attach->bus_setup_intr->ata_pci_setup_intr (ata-pci.c:381-384 non-legacy AHCI branch) writes controller->interrupt[unit].function/.argument 16 bytes per entry NO bounds check. ata_pci_teardown_intr at 403-404 NULLs same OOB path. ata_generic_intr (584-587) AHCI IRQ handler iterates 0..ctlr->channels reading interrupt[unit] OOB then CALLS function ptr with argument on every IRQ. Up to 24*16=384 bytes heap corruption + indirect call through OOB-read function pointers. Requires PCIe hotplug/Thunderbolt or crafted QEMU AHCI emulation presenting CAP.NP>7. Auto-attach default.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1716 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace simulation of ata_pci attach+IRQ loops | 5.6 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -Wextra -o harness harness.c | 100 B | view raw |
| run.sh | run-script | ./harness | 60 B | view raw |
| build.log | build-log | full build output | 43 B | view raw |
| run.log | run-log | full decisive run output | 5.9 KB | view raw |
| env.txt | environment | uname, cc version | 190 B | view raw |
| VERDICT.md | verdict | full narrative | 3.6 KB | β raw |
| fix.diff | suggested-fix | clamp ctlr->channels to 8 in ata_pci_attach | 705 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1716 β nata ata-pci.c OOB interrupt[] write/read
Verdict
REPRODUCED (logic/harness) β bug confirmed by source trace and a
userspace harness that simulates the exact interrupt[8] layout and the
attach/IRQ loops. The bug is real but not live-triggerable on the
default QEMU guest: the guest has no AHCI controller (NATA binds to the
PIIX3 IDE controller with 2 channels) and reaching channels > 8 requires
a malicious / hotplug AHCI controller reporting CAP.NP > 7 or PI bits
8..31 set. On such a controller the bug yields up to 384 bytes of heap
OOB write on attach and OOB function-pointer read+indirect-call on every
IRQ; with no SMAP/SMEP/KASLR + heap grooming this is RIP control.
Mechanism (path:line)
sys/dev/disk/nata/chipsets/ata-ahci.c:101-103βctlr->channels = MAX(flsl(ATA_AHCI_PI), (ATA_AHCI_CAP & ATA_AHCI_NPMASK)+1)sets channels up to 32 from hardware registers.sys/dev/disk/nata/ata-pci.h:64-67βstruct { void (*function)(void*); void *argument; } interrupt[8];is hard-coded to 8 slots.sys/dev/disk/nata/ata-pci.c:230β attach loop addsctlr->channelschildren (up to 32).sys/dev/disk/nata/ata-pci.c:383-384βata_pci_setup_intrwritescontroller->interrupt[unit].function/.argumentwith no bounds check onunitagainst 8.sys/dev/disk/nata/ata-pci.c:584-586βata_generic_intriteratesunit < ctlr->channels, readsinterrupt[unit].argument, then callsinterrupt[unit].function(ch)β OOB read + indirect call on every IRQ.
For channels = 32, units 8..31 each touch 16 bytes of OOB heap
(24 Γ 16 = 384 bytes).
Phase 6 escalation
A live escalation chain on the default guest is not possible: there is
no AHCI device on the QEMU PIIX3-only config, and NATA binds to the legacy
IDE controller with 2 channels. The realistic trigger is a malicious /
hotplug PCIe AHCI device (Thunderbolt, crafted QEMU command line). This
is a valid external-device threat model, not an unprivileged-user
privesc. With such a device the OOB function-pointer read in
ata_generic_intr is direct RIP control (no SMEP, user pages executable),
so the chain would be: spray attacker-shaped heap content where the
controller's OOB interrupt[] region is read, place a userspace
commit_creds(prepare_kernel_cred(0)) trampoline at the address the OOB
function field reads, wait for the next IRQ. We did not develop that
chain because it requires the malicious-device precondition the default
guest cannot supply.
PoC
harness.c simulates the controller struct, attach loop, and IRQ loop
with channels = 32. Reports 24 OOB writes (Phase 1) and 24 OOB
function-pointer reads+indirect-calls per IRQ (Phase 2).
=== DF-1716 nata AHCI OOB interrupt[] harness === ctlr->channels = 32 (from CAP.NP+1 / PI), interrupt[] size = 8 slots Phase 1 result: 24 OOB writes performed (expected 24 for channels=32) Phase 2 result: 24 OOB-read+call per IRQ (expected 24 for channels=32) VERDICT: BUG CONFIRMED. AHCI reporting 32 channels causes 24 OOB writes on attach and 24 OOB function-pointer reads+calls per IRQ (384 bytes heap corruption, full RIP control with heap grooming).
Build: cc -O2 -Wall -Wextra -o harness harness.c Β· Run: ./harness
PoC changes
Original folder had no source. Wrote harness.c, build.sh, run.sh,
this VERDICT.md, manifest.json, and fix.diff.
Fix
fix.diff clamps ctlr->channels to 8 in ata_pci_attach after the
chip-init returns, before the child-attach loop runs. This is the
smallest change that bounds both the attach writes and the IRQ reads
without reworking the fixed-size interrupt[] array. Validated by a
clean nativekernel rebuild (nata is in GENERIC).
Fix verification
fixedVALIDATED the fix: applied fix.diff (DF-1716 + DF-1737 combined nativekernel build since both are in nata/GENERIC) to /usr/src, ran 'make -j6 nativekernel' rc=0, installed kernel.stripped to /boot/kernel/kernel, booted to kern.version #1 (today's ts), sha256 6439b303... differs from baseline 5dc83dac... The patched source contains the new bound check (grep -c DF-1716 == 1). Harness still demonstrates the bug logic for the unbounded case; the patched kernel's ata_pci_attach now caps channels at 8 before the loop can OOB.
baseline: harness reports 24 OOB writes + 24 OOB function-ptr reads/calls per IRQ for channels=32 patched: nativekernel rc=0, boots to #1 (18:08:47 UTC 2026), sha256 6439b303... != baseline 5dc83dac...; ata_pci_attach now clamps ctlr->channels to 8 before attach loop => OOB writes impossible.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- n
- a
- t
- a
- /
- c
- h
- i
- p
- s
- e
- t
- s
- /
- a
- t
- a
- -
- a
- h
- c
- i
- .
- c
- :
- 1
- 0
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- n
- a
- t
- a
- /
- a
- t
- a
- -
- p
- c
- i
- .
- h
- :
- 6
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- n
- a
- t
- a
- /
- a
- t
- a
- -
- p
- c
- i
- .
- c
- :
- 2
- 3
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- n
- a
- t
- a
- /
- a
- t
- a
- -
- p
- c
- i
- .
- c
- :
- 3
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- n
- a
- t
- a
- /
- a
- t
- a
- -
- p
- c
- i
- .
- c
- :
- 5
- 8
- 4
Detail
Exploit chain
External-device threat (malicious/hotplug AHCI controller); not an unprivileged-user privesc on the default guest. With such a device the IRQ handler's OOB read of interrupt[unit].function is direct RIP control on this guest (no SMAP/SMEP, user pages executable, KASLR off): spray attacker-shaped heap content where the controller's OOB interrupt[] region is read, place a userspace commit_creds(prepare_kernel_cred(0)) trampoline at the address the OOB function field reads, wait for the next IRQ. Chain not developed because the malicious-device precondition is absent on the default QEMU PIIX3-only config (no AHCI). Harness in harness.c.
Evidence (decisive lines)
Phase 1 result: 24 OOB writes performed (expected 24 for channels=32) Phase 2 result: 24 OOB-read+call per IRQ (expected 24 for channels=32) legit IRQ dispatches: 8 VERDICT: BUG CONFIRMED. AHCI reporting 32 channels causes 24 OOB writes on attach and 24 OOB function-pointer reads+calls per IRQ (384 bytes heap corruption, full RIP control with heap grooming).
PoC changes
Wrote harness.c (userspace simulation of the controller struct + attach + IRQ loops), build.sh, run.sh, VERDICT.md, manifest.json, fix.diff. Original folder was empty.
Verified recommended fix
fix.diff clamps ctlr->channels to 8 in ata_pci_attach (after chipinit returns, before the child-attach loop), bounding both the attach writes and the IRQ reads without reworking the fixed-size interrupt[] array. Supersedes finding proposal (finding had no concrete patch).
Verdict
REPRODUCED (logic/harness). The OOB interrupt[] write/read is real: ata-ahci.c:101 sets ctlr->channels up to 32, ata-pci.h:67 fixes interrupt[] at 8, ata-pci.c:230 attach loop adds ctlr->channels children, ata-pci.c:383 writes interrupt[unit] with no bounds check, ata-pci.c:584 IRQ loop reads interrupt[unit].function and indirect-calls it. For channels=32, units 8..31 each corrupt 16 bytes of adjacent heap (24x16=384 bytes) and the IRQ handler dereferences attacker-shaped function pointers. Confirmed by a userspace harness that simulates the struct layout and both loops: 24 OOB writes + 24 OOB function-pointer reads+calls per IRQ for channels=32. No live trigger on default QEMU guest (no AHCI device; NATA binds to PIIX3 IDE with 2 channels). Realistic trigger is a malicious/hotplug PCIe AHCI device reporting CAP.NP>7 or PI bits 8..31 set.
No comments yet.