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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2210 Β· 19 files
FileTypeDescriptionSize
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.c is #included verbatim into the KLD (build.sh pins sha256 c694b9095ed2dc50acb932a2ecde564b535c5a0f66cba355a0345d69b16a43e6 = the audited sys/ tree; the only textual intervention renames the never-called ata_national_ident to avoid a duplicate global with the built-in kernel copy β€” ata_national_setmode itself is compiled byte-for-byte under its production name);
  • the running kernel's own ata_national_setmode (local symbol, nm /boot/kernel/kernel β†’ 0xffffffff80321780 on 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 the ATA_STALL_QUEUE state with a hw.begin_transaction that reports the drive accepting SETXFER, so the kernel's own production ata_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 rig
  • build.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=NULL
  • panic_rig_copy.txt β€” serial console: go=3, verbatim copy, dma=NULL
  • kernel_handler_disasm.txt β€” objdump of the stock kernel handler (+0x65 = line 77 store; next store +0x73 = line 78)
  • fix.diff β€” git-apply-able guard
  • fix_build.log / fix_run.log β€” patched-kernel build + validation run
  • env.txt β€” guest environment
  • VERDICT.md β€” full narrative
  • manifest.json / verdict.json β€” machine-readable verdicts
VERDICT.md
↓ download raw

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-78 stores through ch->dma with 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 at ata-national.c:44 β€” guards the same derefs behind if (ctlr->r_res1) { ch->dma->alignment = 16; ... }.
  • ch->dma is NULL-able in production: ata-dma.c:151-154 (ata_dmaalloc error path) prints "WARNING - DMA allocation failed, disabling DMA", frees ch->dma and 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) calls ctlr->setmode(dev, ATA_PIO_MAX);
  • ioctl: IOCATASMODE (ata-all.c:523-526) β†’ ATA_SETMODE newbus method (ata_if.m) β†’ same ata_pcichannel_setmode;
  • device open: acd_open (atapi-cd.c:147), atapi-tape.c:121, atapi-fd.c:107, ata-disk.c:436 β€” all call ATA_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 production chipsets/ata-national.c verbatim (build.baseline.log checker: guest file sha256 c694b9095ed2dc50acb932a2ecde564b535c5a0f66cba355a0345d69b16a43e6 == audited sys/ tree). Only the never-called ata_national_ident is renamed (duplicate global vs the built-in kernel copy); ata_national_setmode compiles 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 = 0xffffffff80321780 stock) β€” 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 in ATA_STALL_QUEUE state with a hw.begin_transaction answering SETXFER-accepted, so the kernel's real ata_controlcmd()/ata_queue_request() (ata-queue.c:158-190, synchronous CONTROL completion at ata-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 sysctl write 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 with mode=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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Oct 17 08:46:06 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.