Unconditional NULL dereference of ch->dma in ata_national_setmode panics kernel when DMA unavailable
Summary
ata_national_setmode unconditionally dereferences ch->dma->alignment and ch->dma->max_iosize at lines 77-78 before any mode negotiation or resource check. ch->dma pointer is NULL whenever PCI busmastering unavailable (ctlr->dmainit never set per ata-pci.c:217-223 BUSMASTEREN bit does not stick) or DMA tag/map allocation fails (ata-dma.c:149-153 sets ch->dma=NULL on error). When any ATA device on channel subsequently configured at boot probe via ad_init/acd_attach or at runtime via IOCATASMODE ioctl (ata-all.c:523-526) or acd_open (atapi-cd.c:226) NULL dereference causes immediate kernel page-fault panic. National driver is documented clone of Cyrix driver (comment line 39) Cyrix original guards this access inside if(ctlr->r_res1) (ata-cyrix.c:84) National dropped that guard. Trigger: local user on system with SC1100 (PCI ID 0x0502100b) ATA controller or VM admin controlling virtual PCI device command register/DMA capabilities. Impact: immediate kernel panic NULL page fault local DoS. Two models: (a) deterministic boot system/VM where SC1100 PCI command register lacks BUSMASTEREN hardwired 0 attach virtual disk panic at probe; (b) probabilistic induce memory pressure cause ata_dmaalloc failure trigger re-probe via IOCATAATTACH or open /dev/acd0. [VERIFIED pass2: reproduced - stock kernel Fatal trap 12 write at vaddr 0x58 = the line-77 store through ch->dma NULL via both shipped handler and sha256-verified verbatim copy; fix-validated on single-fix kernel - guard present, PIO setmode proceeds, dma-present controls bit-identical; NOT a privesc candidate - NULL-page write of constants only]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2210 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.2 KB | β raw | |
| VERDICT.md | β | 7.5 KB | β raw | |
| dfrig_nat.c | β | 15.8 KB | view raw | |
| Makefile | β | 575 B | β download | |
| build.sh | β | 219 B | view raw | |
| run.sh | β | 315 B | view raw | |
| build.baseline.log | β | 6.6 KB | view raw | |
| build.patched.log | β | 6.1 KB | view raw | |
| run.log | β | 3.2 KB | view raw | |
| run_control.log | β | 1.6 KB | view raw | |
| panic.txt | β | 977 B | view raw | |
| panic_rig_copy.txt | β | 977 B | view raw | |
| kernel_handler_disasm.txt | β | 2.2 KB | view raw | |
| fixed_handler_disasm.txt | β | 2.3 KB | view raw | |
| fix.diff | β | 681 B | view raw | |
| fix_build.log | β | 5.7 MB | β download | |
| fix_run.log | β | 2.5 KB | view raw | |
| env.txt | β | 2.1 KB | view raw | |
| verdict.json | β | 6.5 KB | view raw |
DF-2210 β PoC evidence pack (verify run, 2026-10-17)
Finding: sys/dev/disk/nata/chipsets/ata-national.c:77-78 β
ata_national_setmode() unconditionally executes
ch->dma->alignment = 16; /* ata-national.c:77 */
ch->dma->max_iosize = 64 * DEV_BSIZE; /* ata-national.c:78 */
with no NULL guard. Its own template (the Cyrix twin this chip is a
clone of, chipsets/ata-cyrix.c:82-101) guards the identical derefs
behind if (ctlr->r_res1). ch->dma is legitimately NULL in
production: ata-dma.c:151-154 frees ch->dma and NULLs it whenever
DMA allocation fails ("WARNING - DMA allocation failed, disabling
DMA"), after which the channel keeps running PIO-only β and every
later setmode funnel (boot reinit, IOCATASMODE ioctl
ata-all.c:523-526, acd_open atapi-cd.c:147/226) panics the
kernel with a NULL-page WRITE instead of setting PIO mode.
How it was verified (production-handler KLD dispatch rig)
Hardware is gated (National Geode SC1100, PCI ID 0x0502100b), so the
established rig class was used (dfrig_nat.c β same class as DF-252x
trm / DF-249x nata):
- the production
chipsets/ata-national.cis#included verbatim into the KLD (build.shpins sha256c694b9095ed2dc50acb932a2ecde564b535c5a0f66cba355a0345d69b16a43e6= the auditedsys/tree; the only textual intervention renames the never-calledata_national_identto avoid a duplicate global with the built-in kernel copy βata_national_setmodeitself is compiled byte-for-byte under its production name); - the running kernel's own
ata_national_setmode(local symbol,nm /boot/kernel/kernelβ0xffffffff80321780on the stock kernel) is dispatched by address β the shipped binary, zero recompilation; - the fabricated state is exactly what the machine presents when DMA
is unavailable:
ch->dma == NULL, disk present (ch->devices = ATA_ATA_MASTER), PIO4/UDMA2-capable identify data; the channel is in theATA_STALL_QUEUEstate with ahw.begin_transactionthat reports the drive accepting SETXFER, so the kernel's own productionata_controlcmd()/ata_queue_request()run unmodified (synchronous CONTROL completion,ata-queue.c:100-115); PCI config timing writes dispatch through the real kernel kobj machinery onto a recording fake bus; - dispatch is either direct or through the extracted real call site
(
ata_pcichannel_setmode,ata-pci.c:855-866).
Scenarios (sysctl kern.dfrig_nat.go=N)
| go | handler | ch->dma | stock kernel result |
|---|---|---|---|
| 1 | running kernel own copy | NULL | Fatal trap 12, vad 0x58, ata_national_setmode+0x65: movl $0x10,0x58(%rax) (line 77) |
| 2 | running kernel own copy | exists | normal setmode: alignment 2β16, max_iosize 65536β32768, PCI 0x44β0x80000000, 0x40βpiotiming[4], mode=PIO4 |
| 3 | verbatim #included copy | NULL | identical panic (byte-identity) |
| 4 | verbatim #included copy | exists | identical to go=2 |
| 5 | full extracted call site (ata-pci.c:855-866), requested ATA_UDMA2 |
NULL | panic on the first ctlr->setmode call |
| 6 | full extracted call site | exists | two setmode calls; UDMA pass writes udmatiming[2]=0x80911030, mode=ATA_UDMA2 |
Build / run (guest, root; requires vm.sh reset with-src)
cd /root/dfrig_nat && sh build.sh # KLD with verbatim production file
kldload ./dfrig_nat.ko
KADDR=$(nm /boot/kernel/kernel | awk '/ t ata_national_setmode$/{print $1}')
sysctl -w kern.dfrig_nat.addr=0x$KADDR
sysctl -w kern.dfrig_nat.go=2 # CONTROL first (returns)
sysctl -w kern.dfrig_nat.go=1 # THE FINDING β panics a stock kernel
Expected on a stock kernel: Fatal trap 12: page fault while in
kernel mode, fault virtual address = 0x58, supervisor write data, page
not present, Stopped at ata_national_setmode+0x65: movl
$0x10,0x58(%rax) β the store of the constant 16 (0x10) to
offsetof(struct ata_dma, alignment) (0x58) through the NULL ch->dma
β and the guest dead at db>.
Fix validation
fix.diff guards the two stores with if (ch->dma) { ... } (the
canonical guard the Cyrix twin already carries). Single-fix kernel
built with make -j6 nativekernel KERNCONF=X86_64_GENERIC
(fix_build.log); the exact same rig re-run against the patched
kernel (fix_run.log): go=1/go=3/go=5 RETURN β setmode completes in
PIO (mode set, PCI timings written, no panic) β and the go=2/4/6
controls behave identically to stock (dma fields still overridden when
dma exists).
Artifacts
dfrig_nat.c/Makefile/build.sh/run.shβ the rigbuild.logβ full untrimmed baseline build (checker sha lines included)run_control.logβ controls go=2/4/6 on stock kernel (dma exists)panic.txtβ serial console: go=1, kernel's own handler, dma=NULLpanic_rig_copy.txtβ serial console: go=3, verbatim copy, dma=NULLkernel_handler_disasm.txtβ objdump of the stock kernel handler (+0x65= line 77 store; next store+0x73= line 78)fix.diffβ git-apply-able guardfix_build.log/fix_run.logβ patched-kernel build + validation runenv.txtβ guest environmentVERDICT.mdβ full narrativemanifest.json/verdict.jsonβ machine-readable verdicts
DF-2210 β VERDICT
Finding: sys/dev/disk/nata/chipsets/ata-national.c:77-78 β
ata_national_setmode() dereferences ch->dma (->alignment,
->max_iosize) unconditionally; NULL ch->dma (DMA allocation failed,
ata-dma.c:151-154, or busmastering unavailable) turns every later
setmode into a kernel NULL-page WRITE β panic. Medium / local DoS.
Verdict: REPRODUCED (panic), then FIX VALIDATED.
1. Root cause confirmed in source
chipsets/ata-national.c:77-78stores throughch->dmawith no guard, before even issuing SETXFER.- The Cyrix twin (
chipsets/ata-cyrix.c:82-101) β the SC1100 is a documented Cyrix clone, see the ident comment atata-national.c:44β guards the same derefs behindif (ctlr->r_res1) { ch->dma->alignment = 16; ... }. ch->dmais NULL-able in production:ata-dma.c:151-154(ata_dmaallocerror path) prints "WARNING - DMA allocation failed, disabling DMA", freesch->dmaand sets it to NULL; the channel continues PIO-only.- Every setmode funnel reaches the handler:
- boot/reinit:
ata_pcichannel_setmode(ata-pci.c:855-866) callsctlr->setmode(dev, ATA_PIO_MAX); - ioctl:
IOCATASMODE(ata-all.c:523-526) βATA_SETMODEnewbus method (ata_if.m) β sameata_pcichannel_setmode; - device open:
acd_open(atapi-cd.c:147), atapi-tape.c:121, atapi-fd.c:107, ata-disk.c:436 β all callATA_SETMODE.
2. Rig (hardware-gated β production-handler KLD dispatch class)
The SC1100 chipset is absent under QEMU, so per the established rig
class (DF-252x trm, DF-249x nata) dfrig_nat.c:
#includes the productionchipsets/ata-national.cverbatim (build.baseline.logchecker: guest file sha256c694b9095ed2dc50acb932a2ecde564b535c5a0f66cba355a0345d69b16a43e6== auditedsys/tree). Only the never-calledata_national_identis renamed (duplicate global vs the built-in kernel copy);ata_national_setmodecompiles byte-for-byte under its own name.- ALSO dispatches the running kernel's own handler by address
(
nm /boot/kernel/kernelβt ata_national_setmode=0xffffffff80321780stock) β the shipped binary, no recompile. - Fabricates exactly the DMA-unavailable machine state:
ch->dma = NULL, disk present (ch->devices = ATA_ATA_MASTER), PIO4/UDMA2 identify params, channel inATA_STALL_QUEUEstate with ahw.begin_transactionanswering SETXFER-accepted, so the kernel's realata_controlcmd()/ata_queue_request()(ata-queue.c:158-190, synchronous CONTROL completion atata-queue.c:100-115) run unmodified; PCI config writes dispatch through the real kobj machinery onto a recording fake bus. - Dispatches direct or through the extracted real call site
(
ata_pcichannel_setmode,ata-pci.c:855-866).
3. Baseline result (stock kernel, INVARIANTS): panic at :77, twice
go=2/4/6 controls (dma exists) first proved the scaffolding faithful:
alignment 2β16, max_iosize 65536β32768, real ata_controlcmd
dispatch (1 SETXFER), PCI writes 0x44β0x80000000,
0x40β0x00100010 (= piotiming[4]), and for the UDMA2 request
0x44β0x80911030 (= udmatiming[2]), atadev->mode=0x42 β the
production timing tables to the last digit (run_control.log).
Then the DMA-unavailable dispatches:
dfrig_nat[go1]: BEFORE dispatch: ch->dma=0 (NULL means DMA unavailable, ata-dma.c:153) dfrig_nat[go1]: dispatching production ata_national_setmode(dev, ATA_PIO_MAX) (RUNNING KERNEL own copy) Fatal trap 12: page fault while in kernel mode fault virtual address = 0x58 fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff803217e5 Stopped at ata_national_setmode+0x65: movl $0x10,0x58(%rax)
(panic.txt; guest dead at db>, ssh gone, vm.sh status = down.)
Instructionβline attribution is airtight (kernel_handler_disasm.txt):
ffffffff803217de: mov 0x1d8(%r15),%rax ; rax = ch->dma (NULL) ffffffff803217e5: movl $0x10,0x58(%rax) ; LINE 77 alignment=16 <-- faults ffffffff803217ec: mov 0x1d8(%r15),%rax ; reload ch->dma ffffffff803217f3: movl $0x8000,0x64(%rax) ; LINE 78 max_iosize=64*DEV_BSIZE
IP 0xffffffff803217e5 is ata_national_setmode+0x65, the store of
the constant 16 (0x10) to offset 0x58 β exactly the line-77 store
through the NULL ch->dma; vad 0x58 =
offsetof(struct ata_dma, alignment); "supervisor write". The
panic repeats identically for the verbatim #included copy
(panic_rig_copy.txt, go=3: same vad 0x58, same
ata_national_setmode+0x65: movl $0x10,0x58(%rax)), proving the
rig-compiled code is behaviorally identical to the shipped binary.
Impact: NULL-page write of fully controlled constants (16 / 32768) at fixed offsets 0x58/0x64 β not mappable on amd64 (page 0 is hard-wired unmappable), so the primitive is a deterministic local kernel panic / reboot-candidate (DoS), not an escalation. No route to uid=0.
4. Fix validation (single-fix kernel)
fix.diff guards the two stores:
if (ch->dma) {
ch->dma->alignment = 16;
ch->dma->max_iosize = 64 * DEV_BSIZE;
}
β the same protection the Cyrix original already carries
(ata-cyrix.c:82), applied to the National code path that has no
r_res1 resource (its register access is PCI config of the
grandparent, which stays valid; only the ch->dma writes needed the
guard).
Rebuilt with make -j6 nativekernel + installkernel
(fix_build.log, kernel #1: Sat Oct 17 08:46:06 UTC 2026, sha256
d578088322657bb02e15787837bad2582c89869a8cf55af1f7c5208c1795a075),
rebooted, rig rebuilt from the patched source (checker notes the new
sha 6efe3d30...), and the exact same scenarios re-run
(fix_run.log):
| scenario | stock kernel | fixed kernel |
|---|---|---|
| go=1 kernel own handler, dma=NULL | fatal trap 12, vad 0x58, +0x65 |
RETURNED: PIO setmode completes, mode=0x0c, PCI timings written, SETXFER via real ata_controlcmd |
| go=3 verbatim copy, dma=NULL | identical panic | RETURNED, identical to go=1 |
| go=5 full call site, dma=NULL, UDMA2 req | panic on first ctlr->setmode |
both calls complete: PIO pass + 0x44βudmatiming[2], mode=0x42 |
| go=2/4/6 controls, dma exists | normal setmode (16/32768/tables) | identical to stock β guard changes nothing when dma exists |
Patched-kernel disassembly (fixed_handler_disasm.txt) shows the
guard in the binary: mov 0x1d8(%r15),%rax; test %rax,%rax; je
+0x74 skipping both stores.
Baseline reproduced = 1 (twice), patched reproduced = 0. Fix status: fixed.
5. 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 state. All code paths exercised are production bytes; the only fabrications are the device graph, the SETXFER answer, and the PCI config backing store β none of which sit between the cited deref and its NULL input.
- The panic occurs in
sysctlwrite context on the rig; on real hardware the identical fault fires from the boot reinit / ioctl / open funnels cited above (all reach the same first call withmode=ATA_PIO_MAX). - Not a privilege escalation: constant-only NULL-page write (page 0 unmappable on amd64). Classified panic/DoS.
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
fixedApplied fix.diff inside the guest /usr/src, rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel (fix_build.log, kernel sha256 d578088322657bb02e15787837bad2582c89869a8cf55af1f7c5208c1795a075), rebooted, rebuilt the rig from the patched source, and re-ran the exact scenarios: the dispatch that panicked the stock kernel (kernel own handler, ch->dma=NULL) now RETURNS with PIO setmode completing normally (atadev->mode=0x0c, SETXFER accepted through the real kernel ata_controlcmd, PCI timings written); the full extracted call site completes both calls including the UDMA2 pass; all dma-present controls remain bit-identical to stock (alignment 16, max_iosize 32768, same timing-table writes). Patched binary shows the guard: test rax,rax / je before both stores.
fix_run.log: dfrig_nat[go1] RETURNED (no panic) with ch->dma=0, mode set, pci writes recorded - versus panic.txt on stock: fatal trap 12 vad 0x58 at ata_national_setmode+0x65. fixed_handler_disasm.txt vs kernel_handler_disasm.txt: unguarded stores vs test-and-skip.
Confirmed kernel references
- sys/dev/disk/nata/chipsets/ata-national.c:77
- sys/dev/disk/nata/chipsets/ata-national.c:78
- sys/dev/disk/nata/ata-dma.c:151
- sys/dev/disk/nata/ata-dma.c:153
- sys/dev/disk/nata/ata-pci.c:855
- sys/dev/disk/nata/ata-pci.c:863
- sys/dev/disk/nata/ata-all.c:523
- sys/dev/disk/nata/ata-queue.c:100
- sys/dev/disk/nata/chipsets/ata-cyrix.c:82
- sys/dev/disk/nata/atapi-cd.c:147
Detail
Exploit chain
None - NULL-page WRITE of fixed constants (16 at +0x58, 32768 at +0x64) through a NULL ch->dma; the first page is hard-unmappable on amd64, so the primitive is a guaranteed kernel panic (local DoS), not controllable memory corruption and not an escalation candidate.
Evidence (decisive lines)
panic.txt: go1 serial console - BEFORE dispatch ch->dma=0 marker, then Fatal trap 12, fault virtual address 0x58, supervisor write data, Stopped at ata_national_setmode+0x65: movl $0x10,0x58(%rax). panic_rig_copy.txt: identical panic from the verbatim #included copy (go3). kernel_handler_disasm.txt: stock kernel objdump attributing +0x65 to the line-77 store and +0x73 to line 78. run_control.log: dma-present controls with production timing-table values. fix_run.log: same scenarios on the patched kernel all RETURN, controls bit-identical. fixed_handler_disasm.txt: test rax,rax / je guard in the patched binary. build.baseline.log: checker sha match c694b909... proving the verbatim compile.
PoC changes
No seed PoC existed (empty pack); built from scratch. Design iteration: the first attempt defined an interposed ata_controlcmd seam, which made the RUNNING-KERNEL dispatch hang (the kernel own ata_controlcmd binds to the kernel own ata_queue_request, which tsleeps on the fabricated channel); replaced by the faithful channel-state fabrication - ATA_STALL_QUEUE state plus hw.begin_transaction returning ATA_OP_FINISHED - which lets the kernel own production ata_controlcmd and ata_queue_request run and complete synchronously (ata-queue.c:100-115).
Verified recommended fix
Guard the two stores with if (ch->dma) { ch->dma->alignment = 16; ch->dma->max_iosize = 64 * DEV_BSIZE; } - the same protection the Cyrix twin carries at ata-cyrix.c:82.
Verdict
Reproduced on the stock INVARIANTS guest with the production-handler KLD dispatch rig: the production chipsets/ata-national.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 0xffffffff80321780) both execute ata_national_setmode line 77 with ch->dma == NULL - the exact state ata-dma.c:151-154 leaves after DMA allocation fails (channel continues PIO-only). Result: Fatal trap 12 page fault while in kernel mode, fault virtual address 0x58 = offsetof(struct ata_dma, alignment), supervisor WRITE data, Stopped at ata_national_setmode+0x65: movl $0x10,0x58(%rax) - the store of the constant 16 through the NULL ch->dma, i.e. precisely cited line 77 (next store movl $0x8000,0x64(%rax) is line 78). Guest dead at db> prompt, ssh gone. Controls with ch->dma present complete the identical production path faithfully (dma defaults 2/65536 overridden to 16/32768, real kernel ata_controlcmd/ata_queue_request run via the ATA_STALL_QUEUE synchronous CONTROL completion, PCI timing writes match the production tables to the digit: piotiming[4]=0x00100010, udmatiming[2]=0x80911030, mode=PIO4/UDMA2). Reachability on real hardware is every setmode funnel: boot reinit and IOCATASMODE ioctl (ata-all.c:523-526) and acd_open (atapi-cd.c:147) all funnel into ata_pcichannel_setmode (ata-pci.c:855-866) whose first act is ctlr->setmode(dev, ATA_PIO_MAX). Impact ceiling: deterministic local kernel panic / DoS - the write is a constant-only NULL-page write at fixed offsets (page 0 is unmappable on amd64), no escalation path. Single-fix kernel (if (ch->dma) guard around the two stores, mirroring the Cyrix twin guard at ata-cyrix.c:82) makes the identical dispatch RETURN with PIO setmode completing normally and leaves all dma-present controls bit-identical to stock.
No comments yet.