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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1891 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict full verification narrative
↓ download 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: cap channels at nitems(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_testable
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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)
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 19:12:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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).