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

gdtexecuteccb: unbounded physical scatter/gather copy β€” CAM_SCATTER_VALID|CAM_SG_LIST_PHYS CCBs with sglist_cnt > GDT_MAXSG overflow the 324-byte gccb->gc_cmd (pass(4)-reachable heap overflow)

Summary

For XPT_SCSI_IO CCBs with CAM_SCATTER_VALID and CAM_DATA_PHYS clear, the raw/cache paths pass the CCB's segment pointer and sglist_cnt verbatim to gdtexecuteccb, which copies nseg 8-byte entries into the SG region sized for exactly 32 entries at the tail of gc_cmd[324] with no bound. The bus_dmamap nsegments constraint only bounds the map-load paths. A root process using /dev/passN (0600) can submit a physical SG list with arbitrary sglist_cnt - 64 entries writes ~256 bytes of attacker-chosen pairs past gc_cmd into gc_cmd_len, the SLIST link, and neighbouring gccbs. PoC built and run on the guest to the hardware gate (SIM check correctly stopped at 'ata' != 'iir'). Reject-oversized-SG fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4174 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.3 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
sg_overflow.c β€” 4.5 KB view raw
gate_check.sh β€” 779 B view raw
build.log β€” 23 B view raw
run.log β€” 250 B view raw
gate_check.log β€” 577 B view raw
build_and_run.out β€” 273 B view raw
env.txt β€” 1.2 KB view raw
fix.diff β€” 732 B view raw
manifest.json β€” 1002 B view raw
verdict.json β€” 2.7 KB view raw

DF-4174 β€” iir(4) unbounded physical scatter/gather copy (pass(4)-reachable heap overflow)

Build

# on a DragonFly guest:
cc -O -Wall -o sg_overflow sg_overflow.c -lcam

Run

./sg_overflow [pass-device]     # default /dev/pass0

The PoC opens the pass device, verifies via XPT_PATH_INQ that it sits on an iir bus, and then sends an XPT_SCSI_IO CCB with flags = CAM_DIR_IN | CAM_SCATTER_VALID | CAM_SG_LIST_PHYS and sglist_cnt = 64 (GDT_MAXSG is 32) β€” reproducing exactly the path gdt_raw_cmd() iir.c:843-878 β†’ gdtexecuteccb() iir.c:1254-1285 that copies nseg entries into the fixed 32-entry SG array at the tail of the 324-byte gccb->gc_cmd[].

Expected output

  • On a host with iir hardware (as root): kernel heap corruption immediately past gccb->gc_cmd[] into struct gdt_ccb fields and the neighbouring gccbs of the kmalloc'd sc_gccbs[] block (panic or silent corruption). The write content is attacker-chosen segment addresses/lengths.
  • On the audit guest (no iir controller): the SIM check trips β€” /dev/pass0 sits on SIM "ata" β†’ [!] SIM is not "iir" β†’ exit 4. This is the honest hardware gate; see run.log and gate_check.log.

Fix

fix.diff rejects sglist_cnt > GDT_MAXSG physical SG lists in gdt_next() (same place the driver already rejects cdb_len > 12).

VERDICT.md
↓ download raw

DF-4174 VERDICT β€” unbounded physical SG-list copy in gdtexecuteccb (pass(4)-reachable)

Status: untested on this guest (hardware-gated: requires an iir controller to own the CAM bus). PoC built and executed to the hardware gate; bug existence certain from code trace.

Reproduced?

No. The PoC (sg_overflow.c) compiled cleanly on the audit guest (cc -O -Wall … -lcam, build.log: BUILD_RC=0) and ran correctly: it opened /dev/pass0, performed XPT_PATH_INQ, found the SIM is "ata" (the QEMU DVD), refused to fire, and exited 4 (run.log). With no iir controller the vulnerable code is unreachable β€” the gate is documented in gate_check.log (no /dev/iir, no 0x1119/Intel-0600 PCI device, iir.ko not loaded).

Code trace (why the bug is certain)

  1. sc_buffer_dmat is created with nsegments = GDT_MAXSG (=32) (iir.c:202-208, iir.h:71). This bounds only the bus_dmamap_load() paths.
  2. gdt_next() (iir.c:697-780) dispatches XPT_SCSI_IO CCBs to gdt_raw_cmd() (iir.c:782-885) / gdt_cache_cmd() (iir.c:887-989). For a CCB with CAM_SCATTER_VALID set and CAM_DATA_PHYS clear, the guards at iir.c:867-873 panic on other flag combos but the CAM_SG_LIST_PHYS combination falls through to:

    segs = (struct bus_dma_segment )ccb->csio.data_ptr; gdtexecuteccb(gccb, segs, ccb->csio.sglist_cnt, 0); / iir.c:876-877 */

(identically in the cache path, iir.c:982-984). 3. gdtexecuteccb() (iir.c:1234-1304) then loops

   for (i = 0; i < nseg; ++i)
       gdt_enc32(gccb->gc_cmd + GDT_CMD_UNION + GDT_RAW_SG_LST +
                 i * GDT_SG_SZ + GDT_SG_PTR, dm_segs->ds_addr);
       ... GDT_SG_LEN, dm_segs++ ...

(iir.c:1271-1284; cache twin iir.c:1255-1269) β€” no bound on nseg, writing 8 bytes per entry into the SG region that GDT_CMD_SZ / GDT_RAW_SG_LST size for exactly GDT_MAXSG=32 entries (iir.h:234-243: GDT_CMD_SZ = 0x0a + 0x3a + 32*8 = 324). 4. sglist_cnt and data_ptr come verbatim from the CCB. /dev/passN (mode 0600, root only β€” gate_check.log shows the permission bits on the guest's pass0) lets a process submit arbitrary XPT_SCSI_IO CCBs with user-chosen flags, so sglist_cnt = 64 writes 256 bytes past the end of the 32-entry array β€” overflowing gc_cmd[324] into gc_cmd_len, sle link, and the subsequent struct gdt_ccbs of the kmalloc'd sc_gccbs[] block (iir.c:250). Content is the attacker's segment addresses/lengths (64-bit values truncated to 32-bit wire fields).

Reachability / privilege

root + iir hardware (pass device on the iir bus). Not reachable by an unprivileged user. Severity Low per rubric; bucket: memcorrupt.

PoC changes vs seed

The orchestrator's seed sketch used FreeBSD-isms that do not exist on DragonFly: no cam_fill_csio (fields set manually), cam_open_btl() takes a path_id_t not a bus-name string (PoC opens the pass device and verifies the SIM via XPT_PATH_INQ cpi.dev_name), MSG_SIMPLE_Q_TAG/SSD_FULL_LEN removed, and userland struct bus_dma_segment replaced with a local 16-byte {addr,len} struct matching the kernel's x86_64 layout.

Fix validation

fix.diff (reject sglist_cnt > GDT_MAXSG in gdt_next, mirroring the existing cdb_len > 12 rejection) passes git apply --check. Runtime fix validation needs an iir bus; fix_status = not_testable.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff adds the GDT_MAXSG rejection in gdt_next and passes git apply --check; runtime validation requires an iir bus

findings/poc/DF-4174/fix.diff (git apply --check clean)
↓ fix.diffper-fix-DF-4174

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (SIM check trips: SIM is not "iir", exit 4); build.log (BUILD_RC=0); VERDICT.md (path:line trace); fix.diff (git apply --check clean)

PoC changes

rewrote seed against DragonFly camlib: no cam_fill_csio (manual CCB fill), cam_open_btl takes path_id_t not a bus-name string (open pass device + XPT_PATH_INQ cpi.dev_name check instead), dropped MSG_SIMPLE_Q_TAG/SSD_FULL_LEN, local 16-byte {addr,len} struct for userland bus_dma_segment

Verified recommended fix

Reject physical S/G lists with sglist_cnt > GDT_MAXSG in gdt_next before dispatch, mirroring the existing cdb_len > 12 rejection (see fix.diff)

Verdict

Bug certain from code trace; PoC built and executed to the hardware gate. gdtexecuteccb copies nseg physical S/G entries verbatim into the fixed 32-entry (GDT_MAXSG) array at the tail of the 324-byte gccb->gc_cmd (iir.c:1271-1284 raw, 1255-1269 cache) with no bound on nseg; the CAM_SCATTER_VALID|CAM_SG_LIST_PHYS branch of gdt_raw_cmd/gdt_cache_cmd (iir.c:876-877, 983-984) takes sglist_cnt straight from the CCB, so a pass(4) caller (root-only, /dev/passN mode 0600) sending sglist_cnt=64 overflows ~256 bytes into the adjacent struct gdt_ccb fields of the kmalloc'd sc_gccbs block. On this guest the PoC compiled cleanly (build.log BUILD_RC=0) and ran to the SIM check: /dev/pass0 sits on SIM 'ata', so the trigger cannot be delivered (run.log, exit 4); no iir controller exists (gate_check.log).