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

sg(4) SG_IO submits CCBs with data_ptr=NULL and dxfer_len>0: deterministic NULL-page-write kernel panic in nata/ahci transports and bus_dmamap_load(NULL) PRDT at physical address 0

Summary

dxferp==NULL with dxfer_len>0 is never rejected: sgsendccb only maps when data_ptr!=NULL, so the CCB reaches the SIM with NULL data - nata's INQUIRY path bzero(NULL,512) = Fatal trap 12 supervisor write va 0 (reproduced 2/2 identical IP on the ABI-fix kernel), ahci twins bzero + bus_dmamap_load(NULL,len) building a PRDT at PHYSICAL address 0 (device DMA at fixed page 0 / phys-0 disclosure to device). Blocked on stock kernels only by the sibling DF-5402 ABI bug - fixing DF-5402 alone activates this. Reject-the-inconsistent-pair fix validated (EINVAL, sanity path unaffected). Root/operator-gated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5403 Β· 11 files
FileTypeDescriptionSize
poc5402.c poc-source null mode = trigger; sanity = control 5.6 KB view raw
build.sh build in-guest compile 81 B view raw
run.sh run sanity then null 178 B view raw
panic.abifixed.txt panic Fatal trap 12 va 0x0, memset+0xd5, process poc5402 2.1 KB view raw
run.null-trigger-hang.log run-log ioctl never returns; ssh session dies 168 B view raw
run.abifixed-sanity.log run-log same kernel, real-buffer SG_IO works 377 B view raw
run.patched.log run-log full-fix kernel: EINVAL, no panic, guest up 687 B view raw
fix.diff suggested-fix ABI indirection + NULL-data rejection 2.4 KB view raw
env.txt environment guest uname/controller/toolchain 501 B view raw
VERDICT.md verdict choreography + fix validation 3.7 KB ↓ raw
verdict.json verdict machine verdict 5.1 KB view raw
VERDICT.md verdict choreography + fix validation
↓ download raw

DF-5403 VERDICT β€” REPRODUCED (certain)

Impact: panic (deterministic kernel NULL-page write; latent DMA-to-phys-0 variant on ahci) Status: reproduced on the guest via a two-kernel choreography, with fix validation (bad behavior gone with the fix and only the fix).

Mechanism (source)

  1. sys/bus/cam/scsi/scsi_sg.c:502-560 β€” SG_IO accepts dxferp == NULL with dxfer_len > 0; sgsendccb() at sys/bus/cam/scsi/scsi_sg.c:868-869 skips cam_periph_mapmem() exactly when data_ptr == NULL and never rejects the pair.
  2. sys/bus/cam/cam_xpt.c:2977-3047 (xpt_scsi_io) β€” no validation of data_ptr/dxfer_len; CCB queued to the SIM.
  3. Sink (this guest's rig): sys/dev/disk/nata/atapi-cam.c:561 buf = csio->data_ptr; β†’ :582 bzero(buf, len); in the INQUIRY special case (fires when the CDB allocation length > 36). Second sink: atapi-cam.c:756 bcopy(..., csio->data_ptr, len) on odd-length DIR_IN completion. ahci twins: sys/dev/disk/ahci/ahci_cam.c:1083/1097 (bzero), and for commands that reach ahci_ata_cmd β†’ ahci_load_prdt (ahci.c:2025-2034) β†’ bus_dmamap_load(NULL, len) β†’ _bus_dmamap_load_buffer β†’ _bus_dma_extract(pmap_extract(kpm,0)=0) β†’ PRDT segment at physical address 0 (busdma_machdep.c:568-574, 677+): device-controlled DMA into physical page 0 (DIR_IN) or leak of physical page 0 to the device (DIR_OUT).

Runtime choreography (all on DragonFly 6.5-DEVELOPMENT guest,

QEMU PIIX3 ATAPI DVD = sg0 at scbus1; root)

Kernel sanity SG_IO null SG_IO (dxferp=NULL, len=512)
stock #0 Jun 29 EFAULT (DF-5402 blocks entry) EFAULT (cannot reach)
#0 Oct 4 07:01 β€” DF-5402 ABI fix only rc=0, "QEMU DVD-ROM" Fatal trap 12: page fault, fault virtual address = 0x0, supervisor write data, Stopped at memset+0xd5 (inlined bzero), process = poc5402 (panic.abifixed.txt)
#2 Oct 4 07:33 β€” ABI fix + NULL-data validation (fix.diff) rc=0, unchanged rc=-1 errno=22 (EINVAL), guest stays up (run.patched.log)

The panic on the ABI-fixed kernel proves the sg→xpt→atapi-cam path end-to-end: the only thing between a working SG_IO and the NULL-page write is the missing pair validation in sg. The patched kernel returns EINVAL and the sanity path is unaffected (no false positives).

An intermediate kernel #1 that contained the validation before the direction switch (dead check, dir not yet assigned β€” caught by gcc -Werror=maybe-uninitialized during the first fix attempt) still panicked; relocating the check after the switch killed the panic. This confirms the sink is exactly where the trace says it is.

Exploit chain

Root/operator-gated device (0600 + SYSCAP_RESTRICTEDROOT in sgopen), so the primitive is kernel robustness, not unpriv→root: deterministic panic (nata/ahci bzero) or, on ahci SATA with non-INQUIRY commands, device-controlled DMA at physical address 0 (bus_dmamap_load(NULL) → phys-0 segment). uid=0 escalation was not pursued: the write target is fixed (address 0 / phys 0), not attacker-steerable; on this guest page 0 is unmapped for the kernel (immediate trap) — impact is DoS.

Fix

fix.diff (part 2): reject dir != CAM_DIR_NONE && dxferp == NULL && dxfer_len != 0 with EINVAL after the direction switch in the SG_IO case. Validated rc=0 kernel build (gcc 8.3 -Werror), behavior table above.

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 reproduced

Baseline (ABI-fix-only kernel #0): Fatal trap 12 write va 0x0 in memset/bzero <- atapi INQUIRY case, 2/2 trigger runs (once via the 3-mode run, once standalone; identical IP ffffffff80bca655). Patched (kernel #2 = ABI fix + NULL-data validation, make -j6 nativekernel rc=0 under -Werror, installkernel, reboot): same PoC returns rc=-1 errno=22 (EINVAL), no trap, guest stays up, sanity SG_IO still returns drive INQUIRY (no false positives).

run.patched.log (EINVAL, poc-exit=0, guest 'up'); panic.abifixed.txt vs absence of any trap in run.patched.log; fix.diff (part 2 = the validation); VERDICT.md behavior table.
↓ fix.diffper-fix-DF-5403

Confirmed kernel references

Detail

Exploit chain

open(/dev/sg0) as root -> SG_IO{dxfer_direction=FROM_DEV, cmdp=INQUIRY(12 00 00 00 ff 00), dxferp=NULL, dxfer_len=512} -> sgioctl accepts, cam_fill_csio(data_ptr=NULL, dxfer_len=512) -> sgsendccb skips mapmem (data_ptr==NULL) -> xpt_scsi_io (no validation) -> atapi_action XPT_SCSI_IO -> INQUIRY case (alloc len 0xff > 36) -> bzero(NULL, 512) -> Fatal trap 12 write va 0x0 -> panic. Non-INQUIRY variant on ahci: bus_dmamap_load(NULL, len) -> phys-0 PRDT -> DMA at physical address 0.

Evidence (decisive lines)

panic.abifixed.txt (serial console: Fatal trap 12, fault virtual address 0x0, supervisor write data, Stopped at memset+0xd5, process = poc5402); run.null-trigger-hang.log (ioctl never returns); run.abifixed-sanity.log (same kernel: real-buffer SG_IO works, so the CCB path is healthy except for the NULL pair); run.patched.log (full-fix kernel #2: rc=-1 errno=22 EINVAL, guest stays up, sanity unchanged); kernel #1 experiment in VERDICT.md (dead-check placement still panicked, confirming the sink).

PoC changes

poc5402.c written from scratch (no seed); 'null' mode is the trigger, 'sanity' mode proves the same kernel runs well-formed SG_IO fine. Initial trigger attempt on the stock kernel returned EFAULT, which led to discovering the DF-5402 ABI blocker; verification then used an ABI-fix-only kernel to un-mask this bug.

Verified recommended fix

In sgioctl's SG_IO case, after the direction switch, reject dir != CAM_DIR_NONE && req.dxferp == NULL && req.dxfer_len != 0 with EINVAL (fix.diff part 2); land together with the DF-5402 ABI indirection fix.

Verdict

sg(4) never rejects SG_IO with dxferp==NULL and dxfer_len>0: sgsendccb() (scsi_sg.c:868-869) only runs cam_periph_mapmem() when data_ptr != NULL, so the CCB reaches the SIM with data_ptr==NULL and dxfer_len>0, and the transports dereference it. On the stock QEMU PIIX3 ATAPI rig the nata transport's INQUIRY special case does bzero(csio->data_ptr=NULL, dxfer_len) (atapi-cam.c:561->582) - deterministic Fatal trap 12, supervisor write to address 0x0, 'Stopped at memset+0xd5' (inlined bzero), reproducible at will. On ahci SATA disks the same synthesized-INQUIRY bzero exists (ahci_cam.c:1083->1097), and commands that instead reach ahci_ata_cmd/ahci_load_prdt make bus_dmamap_load(NULL, len) build a PRDT segment at PHYSICAL address 0 (pmap_extract fails, _bus_dma_extract returns 0), i.e. device-controlled DMA into physical page 0 (DIR_IN) or disclosure of physical page 0 to the device (DIR_OUT). On a stock kernel the bug is latent ONLY because the sibling ABI bug DF-5402 blocks the SG_IO entry with EFAULT; it was reproduced on an ABI-fix-only kernel and the panic disappears with the NULL-data validation and only that change (EINVAL returned, sanity SG_IO unaffected). Device is root/operator-gated, so this is a root-triggerable kernel panic (same gate as DF-2281) rather than an unpriv escalation.