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)
PoC verification
Evidence pack
findings/poc/DF-4174 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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[]intostruct gdt_ccbfields and the neighbouring gccbs of thekmalloc'dsc_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; seerun.logandgate_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).
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)
sc_buffer_dmatis created withnsegments = GDT_MAXSG(=32) (iir.c:202-208, iir.h:71). This bounds only thebus_dmamap_load()paths.-
gdt_next()(iir.c:697-780) dispatches XPT_SCSI_IO CCBs togdt_raw_cmd()(iir.c:782-885) /gdt_cache_cmd()(iir.c:887-989). For a CCB withCAM_SCATTER_VALIDset andCAM_DATA_PHYSclear, the guards at iir.c:867-873 panic on other flag combos but theCAM_SG_LIST_PHYScombination 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_testablefix.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)
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).
No comments yet.