Unbounded ctlr->channels from AHCI CAP/PI registers: source-side of interrupt[] OOB (variant of DF-1716)
Summary
ata_ahci_chipinit L101-103 ctlr->channels=MAX(flsl(ATA_AHCI_PI),(ATA_AHCI_CAP&ATA_AHCI_NPMASK)+1) directly from hardware registers no upper bound. PI 32-bit bitmap flsl 0..32; CAP.NP 0x1f+1=1..32. Downstream sink interrupt[8] ata-pci.h:67 XXX SOS max ch# for now. channels>8: ata_pci_attach loops unit=0..channels-1 L230 ata_pci_setup_intr writes controller->interrupt[unit] L383-384 past array OOB write (channels-8)*16 bytes. ata_generic_intr L584-586 reads OOB calls corrupted function pointer. Source-side half of DF-1716. Trigger: malicious/glitched PCIe AHCI device CAP.NP=0x1f or PI=0xFFFFFFFF; VFIO pci-passthrough crafted AHCI BAR; custom QEMU ahci ports=32; some legit server AHCI >8 ports. Default config generic ATA/PCI progif=0x01. Impact: 16-byte-aligned heap OOB write up to 384 bytes + OOB read with function-pointer call on every IRQ -> kernel arbitrary code execution/panic. Fix: cap channels at nitems(ctlr->interrupt) using MIN().
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1891 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness reproducing the buggy arithmetic/control-flow | 3.4 KB | view raw |
| VERDICT.md | verdict | full verification narrative | 2.9 KB | β raw |
| build.sh | build-script | exact build command | 88 B | view raw |
| run.sh | run-script | exact run invocation | 41 B | view raw |
| harness_run.log | run-log | harness output on guest | 559 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff | 828 B | view raw |
| env.txt | environment | guest uname, cc version, kernel config | 768 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-1891 β Verification Verdict
Verdict: REPRODUCED (source-confirmed + index-harness)
The unbounded ctlr->channels from CAP/PI registers is confirmed at
sys/dev/disk/nata/chipsets/ata-ahci.c:101-103. The harness reproduces
the 192-byte OOB write into ctlr->interrupt[8] for a 32-port AHCI.
Mechanism
// ata-ahci.c:101-103
ctlr->channels =
MAX(flsl(ATA_INL(ctlr->r_res2, ATA_AHCI_PI)),
(ATA_INL(ctlr->r_res2, ATA_AHCI_CAP) & ATA_AHCI_NPMASK) + 1);
ctlr->channels is taken directly from HW registers with no upper
bound. PI is a 32-bit port bitmap (flsl returns 0..32); CAP.NP is 5
bits (0..31 β +1 = 1..32). The downstream sink ctlr->interrupt[] is
sized 8 (ata-pci.h:67, "XXX SOS max ch# for now"). When channels > 8
(e.g. PI=0xFFFFFFFF β flsl=32, or CAP.NP=0x1f β 32),
ata_pci_setup_intr writes controller->interrupt[unit]
(ata-pci.c:383-384) for unit = 0..channels-1, writing
(channels-8)*sizeof(void*) bytes off the end. With channels=32:
(32-8)*8 = 192 bytes OOB. ata_generic_intr (ata-pci.c:584-586) later
reads these corrupted slots and CALLS through them as function pointers
on every IRQ β kernel arbitrary code execution.
Trigger: a malicious/glitched PCIe AHCI device with CAP.NP=0x1f or
PI=0xFFFFFFFF; VFIO PCI passthrough with a crafted AHCI BAR; custom
QEMU ahci with ports=32; some legitimate server AHCI with >8 ports.
Harness evidence
DF-1891: ata_ahci_chipinit (ata-ahci.c:101-103) AHCI CAP.NP = 0x1f -> 32 ports AHCI PI = 0xffffffff -> flsl = 32 ctlr->channels = 32 (NO upper bound vs interrupt[8]) OOB interrupt[] writes = 24 entries x 8 bytes = 192 bytes past interrupt[8] into the controller struct Harness: simulated writes touched 24 trailer words (function pointers in real kernel) On real HW: ata_generic_intr (ata-pci.c:584-586) later CALLS through these corrupted pointers on every IRQ -> arbitrary kernel code exec.
Why no live trigger on this guest
The audit guest's AHCI controller (the emulated QEMU ahci on the
primary disk) reports a small number of ports, so channels <= 8 and
the OOB doesn't fire. Triggering requires a malicious AHCI BAR
(passthrough / custom QEMU / specific server HBA). Valid Phase-6 hard
blocker.
Exploit chain
Not applicable (needs malicious AHCI HW). No uid=0 claim. Live ceiling
on vulnerable HW: 192-byte heap OOB write + function-pointer call on
every IRQ β arbitrary kernel code execution / panic.
PoC changes
- Added
harness.c: flat model showing 192-byte OOB into interrupt[]. - Added
fix.diff: capchannelsatnitems(ctlr->interrupt).
Fix
fix.diff adds ctlr->channels = MIN(ctlr->channels,
nitems(ctlr->interrupt)) right after the register-derived value is
computed, capping at the actual sink array size.
- BEFORE: harness shows 24 OOB interrupt[] writes (192 bytes).
- AFTER: channels is capped at 8, no OOB.
Fix verification
not_testableVALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: '192 bytes past interrupt[8]' (channels=32) | patched: channels capped at nitems(interrupt)=8
baseline (#0 unpatched): baseline harness: '192 bytes past interrupt[8]' (channels=32) patched (#1 kernel, all 13 fixes, booted clean): patched: channels capped at nitems(interrupt)=8 kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
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
- /
- c
- h
- i
- p
- s
- e
- t
- s
- /
- a
- t
- a
- -
- a
- h
- c
- i
- .
- c
- :
- 1
- 0
- 3
Detail
Exploit chain
HW-gated (guest AHCI reports <=8 ports; trigger needs malicious AHCI BAR with CAP.NP=0x1f or PI=0xFFFFFFFF via VFIO passthrough / custom QEMU). No uid=0 escalation claimed. Primitive characterized in harness.c (flat model showing 192-byte OOB into interrupt[]). Live ceiling: 192-byte heap OOB write + function-pointer call on every IRQ -> arbitrary kernel code exec.
Evidence (decisive lines)
DF-1891: ata_ahci_chipinit (ata-ahci.c:101-103) AHCI CAP.NP = 0x1f -> 32 ports AHCI PI = 0xffffffff -> flsl = 32 ctlr->channels = 32 (NO upper bound vs interrupt[8]) OOB interrupt[] writes = 24 entries x 8 bytes = 192 bytes past interrupt[8] into the controller struct Harness: simulated writes touched 24 trailer words (function pointers in real kernel)
PoC changes
Added harness.c (flat OOB model) and fix.diff (cap channels at nitems(ctlr->interrupt)).
Verified recommended fix
fix.diff adds 'ctlr->channels = MIN(ctlr->channels, nitems(ctlr->interrupt))' after the register-derived value. matches finding proposal exactly. NOTESTABLE: ata-ahci.c is in sys/dev/disk/nata (old NATA, not in conf/files for GENERIC); validated diff applies cleanly + source-inspected.
Verdict
REPRODUCED at source+harness. ata_ahci_chipinit at ata-ahci.c:101-103 sets ctlr->channels = MAX(flsl(AHCI_PI), (AHCI_CAP&NPMASK)+1) directly from HW registers with no upper bound. Sink ctlr->interrupt[] is sized 8 (ata-pci.h:67). Harness with channels=32 (CAP.NP=0x1f or PI=0xFFFFFFFF) shows 24*8=192-byte OOB write into interrupt[8]; ata_generic_intr later calls through corrupted pointers. HW-gated: guest's AHCI controller reports few ports; trigger needs malicious AHCI BAR (VFIO passthrough / custom QEMU / >8-port server HBA).
No comments yet.