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

OOB read on piotiming[] via raw mode&ATA_MODE_MASK index writes garbage PIO timing to disk controller

Summary

Static const piotiming[] array has 5 elements (PIO0-PIO4) but ata_cyrix_setmode indexes it with piotiming[mode & ATA_MODE_MASK] (line 98). Because ATA PIO mode constants start at ATA_PIO0=0x08 (nata.h:203) the masked low nibble is 8-12 reading 3-7 elements (12-28 bytes) past end of 5-element array. Branch reached on every normal device probe: ata_pcichannel_setmode (ata-pci.c:863) calls ctlr->setmode(dev ATA_PIO_MAX) unconditionally inside ata_limit_mode settles on PIO mode always >= ATA_PIO0=0x08. OOB constant written to hardware PIO-timing register via ATA_OUTL programming disk controller with wrong (garbage) timing value causing silent disk data corruption. No info leak to userspace no privilege escalation OOB lands in same .rodata page as three small timing tables cannot fault. Every other nata chipset driver correctly uses ata_mode2idx(mode). Guard at line 84 is SAFE National clone (DF-2210) that dropped it is unsafe one. Data-integrity/reliability defect not memory-corruption exploit primitive. [VERIFIED pass2: reproduced through shipped binary AND verbatim include - PIO4 settles to index 12 reading 28 bytes past the 5-element table with the OOB value cross-checked against live kernel memory then written to the controller PIO register 0x20 as a UDMA2 timing where 0x00040010 belongs, PIO0 index 8 writes 0x00002020; full boot call site leaves both slots with UDMA timings; WDMA UDMA branches in bounds; every probe on Cyrix 5530 affected - silent disk data-integrity defect; deterministic rodata constant never disclosed so privesc closed; ata_mode2idx fix-validated bit-identical]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2213 Β· 18 files
FileTypeDescriptionSize
README.md β€” 8.5 KB ↓ raw
VERDICT.md β€” 11.4 KB ↓ raw
dfrig_cyx.c β€” 20.5 KB view raw
Makefile β€” 572 B ↓ download
build.sh β€” 187 B view raw
run.sh β€” 459 B view raw
build.log β€” 6.1 KB view raw
build.patched.log β€” 6.1 KB view raw
run.log β€” 9.5 KB view raw
dmesg_full.txt β€” 15.8 KB view raw
kernel_handler_disasm.txt β€” 8.0 KB view raw
fixed_handler_disasm.txt β€” 966 B view raw
fix.diff β€” 396 B view raw
fix_build.log β€” 5.7 MB ↓ download
fix_run.log β€” 4.0 KB view raw
env.txt β€” 735 B view raw
fix_env.txt β€” 370 B view raw
verdict.json β€” 8.4 KB view raw

DF-2213 β€” PoC evidence pack (verify run, 2026-10-20)

Finding: sys/dev/disk/nata/chipsets/ata-cyrix.c:98 β€” ata_cyrix_setmode() programs the Cyrix 5530 PIO timing register with

    ATA_OUTL(ch->r_io[ATA_BMCMD_PORT].res,
         0x20 + (devno << 3), piotiming[mode & ATA_MODE_MASK]);

where piotiming[] has five elements (ata-cyrix.c:67-68) but the ATA PIO mode constants start at 0x08 (sys/sys/nata.h:203-207: ATA_PIO0=0x08 .. ATA_PIO4=0x0c) and ATA_MODE_MASK is 0x0f (sys/sys/nata.h:200). For every real PIO mode the masked index is therefore 8..12 β€” reading 3..7 elements (12..28 bytes) past the end of the 20-byte table β€” and the garbage constant so read is written to the controller PIO timing register. In the shipped stock kernel the elements at piotiming[8..12] are dmatiming[2] / a pad hole / udmatiming[0..2] (kernel_handler_disasm.txt), so the standard boot probe (setmode settles at ATA_PIO4) programs a UDMA2 timing (0x00911030) where a PIO4 timing (0x00040010) belongs: wrong setup/hold clocks on every PIO transfer β€” silent disk data-corruption class. Every sibling driver indexes the same shape of table with ata_mode2idx() (ata-pci.c:719-726, the idiom that subtracts ATA_PIO0; e.g. ata-national.c:102, the NatSemi clone of this very driver β€” whose own table is 11 elements wide and padded for exactly that idiom).

How it was verified (production-handler KLD dispatch rig)

Hardware is gated (Cyrix 5530, ATA_CYRIX_5530), so the established rig class was used (dfrig_cyx.c, same class as DF-2210 national, DF-252x trm, DF-249x nata):

  • the production chipsets/ata-cyrix.c is #included verbatim into the KLD (build.sh pins sha256 29f548032b4104baeb4a3eb09009356bc80a0806323597580f5df3b6d041acd9 = the audited sys/ tree; the only textual intervention renames the never-called ata_cyrix_ident to avoid a duplicate global with the built-in kernel copy β€” ata_cyrix_setmode itself is compiled byte-for-byte under its production name);
  • the running kernel's own ata_cyrix_setmode (local symbol, nm /boot/kernel/kernel β†’ 0xffffffff80319490 on the stock kernel) is dispatched by address β€” the shipped binary, zero recompilation;
  • the fabricated state is exactly what a working Cyrix 5530 channel presents: ctlr->r_res1 allocated (the ata-cyrix.c:84 guard passes β€” this file guards its derefs, unlike the National clone of DF-2210), ch->dma present, disk present, PIO0..PIO4/UDMA2 identify data per scenario; the channel is in the ATA_STALL_QUEUE state with a hw.begin_transaction that reports the drive accepting SETXFER, so the kernel's own production ata_limit_mode()/ata_controlcmd()/ ata_queue_request() run unmodified (synchronous CONTROL completion, ata-queue.c:100-115);
  • the ATA_OUTL register writes dispatch through the real bus_space_write_4() onto a fabricated MEM-space bus tag + handle (cpu/x86_64/include/bus_dma.h:404-412: MEM tag β‡’ *(volatile u_int32_t *)(handle+offset) = value), so the production timing writes land in the rig's 128-byte controller register file and are dumped β€” the garbage timing landing in the controller register, captured;
  • the shipped kernel's .rodata piotiming[] base is recovered from the shipped handler's own disassembly (build.sh awk-extracts the absolute-indexed load mov <base>(,%rax,4),%eax of the PIO branch, 0xffffffff80d0cd20 stock) and the rig cross-checks at runtime that [0..4] equal the five source constants before every dump.

Scenarios (sysctl kern.dfrig_cyx.go=N)

go handler disk identify request stock kernel result
1 running kernel own copy PIO4/UDMA2 ATA_PIO_MAX (boot-reinit first call, ata-pci.c:863) settles PIO4 (0x0c) β†’ index 12 OOB β†’ reg 0x20 ← 0x00911030 (= udmatiming[2], a UDMA2 timing); correct piotiming[4]=0x00040010
2 verbatim #included copy PIO4/UDMA2 ATA_PIO_MAX identical to go=1 (byte-identity)
3 running kernel own copy PIO0-only ATA_PIO_MAX settles PIO0 (0x08) β†’ index 8 OOB β†’ reg 0x20 ← 0x00002020 (= dmatiming[2], a WDMA timing); correct piotiming[0]=0x00009172
4 full extracted call site (ata-pci.c:855-866) PIO4/UDMA2 ATA_UDMA2 (a UDMA disk at boot) call 1: reg 0x20 ← 0x00911030 (OOB); call 2: reg 0x24 ← 0x00911030 (in-bounds udmatiming[2], correct) β€” both controller slots end up holding UDMA timings, the PIO one illegitimately
5 running kernel own copy WDMA2-only ATA_WDMA2 reg 0x24 ← 0x00002020 (in-bounds dmatiming[2], correct) β€” the WDMA branch indexes correctly; the defect is specific to the PIO branch (line 98)

Every OOB value is cross-checked against live kernel memory at piotiming + 4*index (OOB read CONFIRMED lines in run.log).

Build / run (guest, root)

cd /root/dfrig_cyx && sh build.sh     # KLD with verbatim production file
kldload ./dfrig_cyx.ko
KADDR=$(nm /boot/kernel/kernel | awk '/ t ata_cyrix_setmode$/{print $1}')
TBL=$(objdump -d --start-address=0x$KADDR --stop-address=$((0x$KADDR + 0x200)) \
      /boot/kernel/kernel | awk '$2=="8b" && $3=="04" && $4=="85" {b=$8$7$6$5}
                                END {print "0xffffffff" b}')
sysctl -w kern.dfrig_cyx.addr=0x$KADDR
sysctl -w kern.dfrig_cyx.tbl=$TBL
sysctl -w kern.dfrig_cyx.go=1         # ... 2 3 4 5

(run.sh does all of this; output lands on the console / dmesg.)

Fix validation

fix.diff replaces the raw mask with the family idiom: piotiming[ata_mode2idx(mode)] (matching ata-national.c:102 and every other sibling). Single-fix kernel built with make -j6 nativekernel KERNCONF=X86_64_GENERIC (fix_build.log, kernel #1: Tue Oct 20 22:39:26 UTC 2026, sha256 bfecc68ad512f8cbfbb2f1447b52411a17f435f891b47a21b1569c3a3abc4fc2); the exact same rig re-built from the patched source and re-run (fix_run.log):

register stock kernel fixed kernel
go1/go2/go4 PIO slot 0x20 0x00911030 (OOB UDMA2 constant) 0x00040010 = piotiming[4] βœ“
go3 PIO slot 0x20 0x00002020 (OOB WDMA constant) 0x00009172 = piotiming[0] βœ“
go4 UDMA slot 0x24 0x00911030 (in-bounds) unchanged βœ“
go5 WDMA slot 0x24 0x00002020 (in-bounds) unchanged βœ“
settled modes / SETXFER count / dma override 16+32768 β€” identical βœ“

The fixed binary shows callq ata_mode2idx feeding the table load (fixed_handler_disasm.txt), versus the stock and $0xf,%eax (kernel_handler_disasm.txt).

Note on reading fix_run.log: the rig prints the hypothetical raw mask index (INDEX USED = mode & ATA_MODE_MASK) as a static annotation in both runs; on the fixed kernel the handler no longer uses it (the register value and the disasm prove it), and the (MISMATCH vs reg 0x20?!) cross-check line is the expected fixed-kernel outcome β€” the register no longer equals the OOB slot because it now carries the in-bounds constant.

Impact classification

.rodata OOB read whose value lands in a hardware register: no kernel memory is disclosed to userspace (the value is a compile-time constant of the shipped kernel), no write primitive beyond the fixed controller register, no control flow influence β€” not an info leak and not a privesc (the team-directive question is closed explicitly: there is no route to uid=0). The impact is a wrong PIO timing programmed on real Cyrix 5530 hardware on every probe (setmode settles β‰₯ PIO0) β€” a data-integrity/reliability defect (silent disk corruption class), persisting Low severity.

Artifacts

  • dfrig_cyx.c / Makefile / build.sh / run.sh β€” the rig
  • build.log β€” full untrimmed baseline KLD build (checker sha lines)
  • build.patched.log β€” rig rebuild from the patched source (checker notes the fixed sha)
  • run.log β€” all five scenarios on the stock kernel (console/dmesg)
  • dmesg_full.txt β€” full guest dmesg of the baseline session
  • kernel_handler_disasm.txt β€” shipped handler disasm + .rodata window (tables and their absolute addresses)
  • fixed_handler_disasm.txt β€” fixed handler disasm (callq ata_mode2idx feeding the PIO table load)
  • fix.diff β€” git-apply-able one-line fix
  • fix_build.log β€” full untrimmed fixed-kernel build
  • fix_run.log β€” the five scenarios on the fixed kernel
  • env.txt / fix_env.txt β€” guest environment (stock / fixed)
  • VERDICT.md β€” full narrative
  • manifest.json / verdict.json β€” machine-readable verdicts
VERDICT.md
↓ download raw

DF-2213 β€” VERDICT

Finding: sys/dev/disk/nata/chipsets/ata-cyrix.c:98 β€” ata_cyrix_setmode() indexes the 5-element piotiming[] table with mode & ATA_MODE_MASK although the PIO mode constants start at 0x08 (sys/sys/nata.h:203-207): every real PIO mode (0x08..0x0c) yields index 8..12 against valid indices 0..4, an OOB .rodata read of 12..28 bytes past the table, whose value is then written to the Cyrix 5530 PIO timing register via ATA_OUTL. Wrong PIO timing on every probe β†’ silent disk data-corruption class. Low / data-integrity.

Verdict: REPRODUCED (OOB read β†’ hardware register), then FIX VALIDATED.

1. Root cause confirmed in source

  • chipsets/ata-cyrix.c:67-68: piotiming[] = 5 uint32_t elements.
  • sys/sys/nata.h:200: ATA_MODE_MASK 0x0f; :203-207: ATA_PIO0 0x08 .. ATA_PIO4 0x0c; so mode & ATA_MODE_MASK ∈ {8,9,10,11,12} for PIO0..PIO4 β€” indices 8..12 on a 0..4 array (3..7 elements past the end).
  • chipsets/ata-cyrix.c:98: piotiming[mode & ATA_MODE_MASK] written to 0x20 + (devno << 3) in the BMIO port space.
  • The sibling idiom subtracts ATA_PIO0: ata_mode2idx() (ata-pci.c:719-726, (mode & ATA_MODE_MASK) - ATA_PIO0 for the PIO range) β€” used by ata-national.c:102 (the NatSemi clone of this driver, whose piotiming[] is 11 elements and padded for exactly this idiom), ata-amd.c:113, ata-intel.c:328, ata-acerlabs.c:276, ata-ati.c:189, ata-ite.c:135, ata-nvidia.c:303, ata-sis.c:253, ata-highpoint.c:174, ata-promise.c:490.
  • The UDMA/WDMA branches of the same function (:90, :94) use mode & ATA_MODE_MASK correctly (UDMA0..2 = 0x40..0x42 and WDMA0..2 = 0x20..0x22 mask to 0..2 on 3-element tables) β€” the defect is specific to the PIO branch (confirmed by the go=5 control).
  • Reachability on real hardware: every setmode funnel calls the handler with ATA_PIO_MAX first (ata_pcichannel_setmode, ata-pci.c:855-866: boot reinit; IOCATASMODE ioctl ata-all.c:523-526; device opens atapi-cd.c:147/226, atapi-tape.c:121, atapi-fd.c:107, ata-disk.c:436), and ata_limit_mode() settles at the disk's best PIO mode (β‰₯ ATA_PIO0 always β€” ata_pmode() floors at ATA_PIO0, ata-all.c:949-968). So every Cyrix 5530 probe with any disk takes the OOB branch.

2. Rig (hardware-gated β†’ production-handler KLD dispatch class)

The Cyrix 5530 chipset is absent under QEMU, so per the established rig class (DF-2210 national, DF-252x trm, DF-249x nata) dfrig_cyx.c:

  • #includes the production chipsets/ata-cyrix.c verbatim (build.log checker: guest file sha256 29f548032b4104baeb4a3eb09009356bc80a0806323597580f5df3b6d041acd9 == audited sys/ tree). Only the never-called ata_cyrix_ident is renamed (duplicate global vs the built-in kernel copy); ata_cyrix_setmode compiles byte-for-byte under its own name.
  • ALSO dispatches the running kernel's own handler by address (nm β†’ t ata_cyrix_setmode = 0xffffffff80319490 stock) β€” the shipped binary, no recompile.
  • Fabricates the working-channel state: ctlr->r_res1 allocated (guard at :84 passes β€” the inversion vs DF-2210 is real: this file guards), ch->dma present, disk present, per-scenario identify data; channel in ATA_STALL_QUEUE with hw.begin_transaction answering SETXFER-accepted, so the kernel's real ata_limit_mode()/ata_controlcmd()/ata_queue_request() (ata-queue.c:100-115,158-190) run unmodified.
  • The ATA_OUTL sink is captured through the real bus_space_write_4 onto a fabricated MEM-space tag + handle (cpu/x86_64/include/bus_dma.h:404-412: MEM tag β‡’ plain volatile store to handle+offset): the production register writes land in a 128-byte rig buffer (the "controller register file") and are dumped.
  • The shipped kernel's .rodata piotiming[] base is recovered from the shipped handler's own code (build.sh extracts the PIO-branch absolute-indexed load; stock: 0xffffffff80d0cd20) and verified at runtime against the five source constants before use.

3. Baseline result (stock kernel, INVARIANTS)

The shipped binary itself encodes the bug β€” kernel_handler_disasm.txt:

ffffffff8031955d:  and    $0xf,%eax                    ; mode & ATA_MODE_MASK
...
ffffffff80319623:  8b 04 85 20 cd d0 80  mov 0x80d0cd20(,%rax,4),%eax  ; piotiming[idx]
ffffffff8031962a:  83 c2 20              add $0x20,%edx                ; PIO timing reg

and the .rodata window at 0xffffffff80d0cd20 (verified live, run.log table dump):

idx value identity
0..4 0x00009172,0x00012171,0x00020080,0x00032010,0x00040010 the real PIO timings (== source)
5 0x00000000 pad hole
6..8 0x00077771,0x00012121,0x00002020 dmatiming[0..2]
9 0x00000000 pad hole
10..12 0x00921250,0x00911140,0x00911030 udmatiming[0..2]

Dispatch results (all through the kernel's own ata_limit_mode + ata_controlcmd, controlcmd_seen=1 per setmode, settled modes as expected):

dfrig_cyx[go1]: requested mode=0x0f settled mode=0x0c
dfrig_cyx[go1]: INDEX USED  = mode & ATA_MODE_MASK = 0x0c & 0x0f = 12  <<< OUT OF BOUNDS
dfrig_cyx[go1]: INDEX RIGHT = ata_mode2idx(mode)   = 4
dfrig_cyx[go1]: CORRECT VALUE piotiming[4] = 0x00040010
dfrig_cyx[go1]:   reg 0x20 (PIO timing, devno 0)  = 0x00911030
dfrig_cyx[go1]: OOB SOURCE kernel_mem[piotiming+12*4] = 0x00911030  == reg 0x20 write (OOB read CONFIRMED)
  • go=1 (kernel's own shipped handler, PIO4 disk, ATA_PIO_MAX β€” the exact first call of the boot-reinit call site): settles 0x0c, index 12, OOB read udmatiming[2] = 0x00911030, written to the PIO timing register 0x20. Correct value: piotiming[4] = 0x00040010. The live-kernel-memory cross-check ties the register value to the OOB slot byte-for-byte.
  • go=2 (verbatim #included copy): identical β€” byte-identity.
  • go=3 (PIO0-only disk): settles 0x08, index 8, OOB read dmatiming[2] = 0x00002020 β†’ reg 0x20; correct piotiming[0] = 0x00009172. Establishes the full claimed range 8..12.
  • go=4 (full extracted call site ata-pci.c:855-866, requested UDMA2, 2 SETXFER = normal): call 1 wrote the OOB value into the PIO slot, call 2 correctly wrote udmatiming[2] into the DMA slot β€” both 0x20 and 0x24 ended up 0x00911030: the controller's PIO timing register holding a UDMA2 timing is the corruption state a real 5530 machine is left in after every boot probe.
  • go=5 CONTROL (WDMA2 request): dmatiming[2] in bounds β†’ reg 0x24 = 0x00002020 β€” the WDMA branch is correctly indexed; the defect is confined to the PIO branch at :98, exactly as claimed.

4. Fix validation (single-fix kernel)

fix.diff (one line, :98):

-            0x20 + (devno << 3), piotiming[mode & ATA_MODE_MASK]);
+            0x20 + (devno << 3), piotiming[ata_mode2idx(mode)]);

β€” the family idiom (national/amd/intel/ite/... all index via ata_mode2idx). Applied inside the guest /usr/src (patched file sha256 d1101d0e381ad25aa03fa598cafed0ddbcaab5b2c748bffa3acbe8f96d81a00f), kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel (fix_build.log, kernel #1: Tue Oct 20 22:39:26 UTC 2026, sha256 bfecc68ad512f8cbfbb2f1447b52411a17f435f891b47a21b1569c3a3abc4fc2), rebooted, rig rebuilt from the patched source (checker notes the new sha), and the exact same scenarios re-run (fix_run.log):

scenario / register stock fixed
go1, go2, go4-call1 β€” PIO slot 0x20 0x00911030 (OOB udmatiming[2]) 0x00040010 = piotiming[4] βœ“
go3 β€” PIO slot 0x20 0x00002020 (OOB dmatiming[2]) 0x00009172 = piotiming[0] βœ“
go4-call2 β€” UDMA slot 0x24 0x00911030 (in bounds) 0x00911030 (unchanged) βœ“
go5 β€” WDMA slot 0x24 0x00002020 (in bounds) 0x00002020 (unchanged) βœ“
settled modes (0x0c/0x08/0x42/0x22), SETXFER counts (1/1/1/2/1), ch->dma override (16/32768) β€” identical to stock βœ“

The fixed binary shows the idiom in machine code (fixed_handler_disasm.txt): callq <ata_mode2idx> feeding mov 0x80d0cd20(,%rax,4),%eax β€” versus the stock and $0xf,%eax. Behavior is otherwise identical (A/B requirement met: correct timing values written, nothing else changes).

Reading note for fix_run.log: the rig still prints the hypothetical raw-mask index (INDEX USED ... <<< OUT OF BOUNDS) as a static annotation in both runs β€” on the fixed kernel the handler demonstrably no longer uses it (disasm + correct register value), and the (MISMATCH vs reg 0x20?!) cross-check line is the expected fixed-kernel outcome: the register no longer equals the OOB slot because it now carries the in-bounds constant.

Baseline OOB reproduced = 1 (five scenarios, two dispatch routes), patched OOB reproduced = 0. Fix status: fixed.

5. Impact classification (team directive: close privesc explicitly)

  • The OOB read source is the shipped kernel's own .rodata (compile-time constants placed by the linker): the read value is deterministic per build, not attacker-influenced, and is never returned to userspace β€” no info leak, no KASLR-relevant disclosure.
  • The write sink is one fixed MMIO register (PIO timing for one drive); no kernel memory is written; no control flow, no refcounts, no lengths involved β€” no memory-corruption primitive, no route to uid=0. The privesc question is closed: not a candidate.
  • Realistic impact: on Cyrix/NatSemi 5530-class hardware (hardware gate AV:P), every probe programs a wrong PIO timing (a UDMA or WDMA constant where a PIO constant belongs β€” e.g. PIO4 gets 0x00911030, whose field layout encodes UDMA cycle/control bits, not PIO recovery/active clocks). Wrong setup/hold timing on PIO transfers is a classic silent disk data-corruption mechanism; no crash, no observable error β€” data-integrity/reliability defect. Persisted severity Low is correct (hardware-gated, no confidentiality or privilege impact).

6. Honesty notes / limitations

  • The PCI ID gate means the shipped binary path cannot attach on this guest; the rig dispatches the shipped handler directly with the post-attach working-channel state. All code paths exercised are production bytes; the only fabrications are the device graph, the SETXFER answer, and the bus-space sink β€” none of which sit between the cited index computation and the register write. The OOB values themselves were cross-checked against the shipped kernel's own .rodata contents at runtime, so the garbage constants are the shipped binary's, not the rig's.
  • The .rodata neighborhood (which constants land at indices 8..12) is a property of this kernel build; other builds may place different constants after the table β€” that does not change the class (any non-PIO constant programmed as PIO timing is wrong), and the index-range evidence (8..12 vs 0..4) is build-independent.
  • Hypothetical mode < ATA_PIO0 (e.g. 0x00) would make ata_mode2idx return a negative index β€” the same caveat every ata_mode2idx sibling carries; unreachable through the real funnels (ata_pmode() floors at ATA_PIO0, all callers pass ATA_PIO_MAX/DMA modes).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff inside the guest /usr/src (one line: piotiming[ata_mode2idx(mode)]), rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel (fix_build.log, kernel sha256 bfecc68ad512f8cbfbb2f1447b52411a17f435f891b47a21b1569c3a3abc4fc2), rebooted, rebuilt the rig from the patched source (checker notes the fixed sha d1101d0e...), and re-ran the exact five scenarios: every PIO-branch register write now carries the correct source constant (go1/go2/go4 reg 0x20 = 0x00040010 = piotiming[4]; go3 reg 0x20 = 0x00009172 = piotiming[0]); the in-bounds UDMA/WDMA writes (go4 reg 0x24 = 0x00911030, go5 reg 0x24 = 0x00002020), settled modes, SETXFER counts and dma overrides are bit-identical to stock. The fixed binary shows callq ata_mode2idx feeding the table load (fixed_handler_disasm.txt) versus the stock and $0xf,%eax.

fix_run.log (A/B against run.log): stock reg 0x20 = 0x00911030 (OOB udmatiming[2]) / 0x00002020 (OOB dmatiming[2]) versus fixed 0x00040010 / 0x00009172 (correct piotiming[4] / piotiming[0]); controls unchanged. fixed_handler_disasm.txt: callq ata_mode2idx before mov 0x80d0cd20(,%rax,4),%eax. fix_build.log: full untrimmed single-fix kernel build. Reading note: the rig prints the hypothetical raw-mask index as a static annotation in both runs; on the fixed kernel the handler demonstrably no longer uses it, and the MISMATCH cross-check line is the expected fixed-kernel outcome (register no longer equals the OOB slot).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Oct 20 22:39:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

None - the OOB read draws a deterministic compile-time constant from kernel .rodata and deposits it in one fixed controller register; no attacker-controlled value, no kernel memory disclosure to userspace, no write primitive beyond the hardware register, no control-flow influence. Not an escalation candidate.

Evidence (decisive lines)

run.log: all five scenarios on the stock kernel - go1 (shipped handler) settled mode=0x0c, INDEX USED = 12 <<< OUT OF BOUNDS, reg 0x20 = 0x00911030 vs CORRECT piotiming[4] = 0x00040010, and OOB SOURCE kernel_mem[piotiming+12*4] = 0x00911030 == reg 0x20 write (OOB read CONFIRMED); go2 identical via the verbatim copy; go3 index 8, reg 0x20 = 0x00002020 vs correct 0x00009172, OOB CONFIRMED; go4 both slots 0x20/0x24 = 0x00911030 after the two production calls; go5 control reg 0x24 = 0x00002020 in bounds. kernel_handler_disasm.txt: stock binary shows and $0xf,%eax then mov 0x80d0cd20(,%rax,4),%eax and the .rodata window mapping indices 8..12 onto dmatiming[2]/hole/udmatiming[0..2]. fix_run.log: same scenarios on the patched kernel write piotiming[4]/piotiming[0] to reg 0x20 with the UDMA/WDMA slots and all other behavior identical. fixed_handler_disasm.txt: callq ata_mode2idx feeding the table load. build.log checker lines: verbatim sha256 29f54803... == audited tree.

PoC changes

No seed PoC existed (empty pack); built from scratch on the DF-2210 rig pattern. Adaptations for this finding: the write sink is ATA_OUTL bus_space (not pci_write_config), so the fabricated PCI kobj bus was replaced by a MEM-space bus tag + kernel-buffer handle which the real bus_space_write_4 stores into (cpu/x86_64/include/bus_dma.h:404-412); the r_res1 guard of this file must pass (ctlr->r_res1 fabricated - this file, unlike the DF-2210 national clone, guards its derefs); a third timing table, the kernel .rodata table base is recovered automatically from the shipped handler disassembly (awk over the absolute-indexed load bytes) and verified at runtime against the five source constants; scenario identify data drives the kernel own ata_limit_mode to PIO4, PIO0, WDMA2 and UDMA2 settles to cover the claimed index range 8..12 plus in-bounds controls.

Verified recommended fix

Index with the family idiom: piotiming[ata_mode2idx(mode)] at ata-cyrix.c:98 (matches ata-national.c:102 and every sibling chipset driver).

Verdict

Reproduced on the stock INVARIANTS guest with the production-handler KLD dispatch rig (DF-2210 rig class): the production chipsets/ata-cyrix.c compiled verbatim into the rig (sha256 checker-verified against the audited sys/ tree) AND the running kernel own shipped handler dispatched by address (nm-local-symbol 0xffffffff80319490) both execute ata_cyrix_setmode line 98 with the mode every real funnel passes first (ATA_PIO_MAX; the handler then settles it via the kernel own ata_limit_mode and ata_controlcmd through the fabricated ATA_STALL_QUEUE synchronous CONTROL completion). For a PIO4-capable disk the settled mode is 0x0c and the computed index mode & ATA_MODE_MASK is 12 against a five-element piotiming table (valid 0..4) - the shipped binary disassembly shows and $0xf,%eax feeding mov 0x80d0cd20(,%rax,4),%eax - so the handler reads kernel .rodata 28 bytes past the table end (udmatiming[2] = 0x00911030 in the shipped build) and writes that UDMA2 timing constant into the controller PIO timing register 0x20 via ATA_OUTL, where the PIO4 constant 0x00040010 belongs; a PIO0-only disk yields index 8 reading dmatiming[2] = 0x00002020 into the PIO register. The rig captures the register write through the real bus_space_write_4 onto a fabricated MEM-space handle (the controller register file) and cross-checks each written value against live kernel memory at piotiming + 4*index - byte-identical match in every OOB scenario (OOB read CONFIRMED). The full extracted call site (ata-pci.c:855-866) with a UDMA2 request shows the boot state on real hardware: both controller slots 0x20 and 0x24 end up holding UDMA timings, the PIO one illegitimately. Controls prove branch specificity: the WDMA and UDMA branches index their three-element tables in bounds (mode constants 0x20..0x22 and 0x40..0x42 mask to 0..2), only the PIO branch at line 98 is wrong, and the correct idiom is ata_mode2idx (ata-pci.c:719-726) used by every sibling including the NatSemi clone of this driver (ata-national.c:102). Impact ceiling per team directive: NOT an info leak (the OOB value is a build-time .rodata constant never returned to userspace), NOT memory corruption reachable by an attacker (write sink is one fixed hardware register with deterministic content), NOT a privesc and NOT a DoS - closed explicitly as no route to uid=0. The realistic impact on Cyrix 5530 hardware is a wrong PIO timing programmed on every probe (setmode settles >= PIO0), a silent disk data-integrity defect; persisted Low severity stands.