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)
PoC verification
Evidence pack
findings/poc/DF-5403 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
sys/bus/cam/scsi/scsi_sg.c:502-560β SG_IO acceptsdxferp == NULLwithdxfer_len > 0;sgsendccb()atsys/bus/cam/scsi/scsi_sg.c:868-869skipscam_periph_mapmem()exactly whendata_ptr == NULLand never rejects the pair.sys/bus/cam/cam_xpt.c:2977-3047(xpt_scsi_io) β no validation ofdata_ptr/dxfer_len; CCB queued to the SIM.- Sink (this guest's rig):
sys/dev/disk/nata/atapi-cam.c:561buf = csio->data_ptr;β:582bzero(buf, len);in the INQUIRY special case (fires when the CDB allocation length > 36). Second sink:atapi-cam.c:756bcopy(..., 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 reachahci_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
fixedBaseline (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.
Confirmed kernel references
- sys/bus/cam/scsi/scsi_sg.c:868
- sys/bus/cam/scsi/scsi_sg.c:543
- sys/bus/cam/cam_xpt.c:2977
- sys/dev/disk/nata/atapi-cam.c:561
- sys/dev/disk/nata/atapi-cam.c:582
- sys/dev/disk/nata/atapi-cam.c:756
- sys/dev/disk/ahci/ahci_cam.c:1083
- sys/dev/disk/ahci/ahci_cam.c:1097
- sys/dev/disk/ahci/ahci.c:2025
- sys/platform/pc64/x86_64/busdma_machdep.c:568
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.
No comments yet.