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

trm_SRBdone auto-sense bcopy uses user-controlled sense_len to copy out of 32-byte DMA slot and into 32-byte csio field (heap OOB read+write via pass(4))

Summary

On auto-request-sense completion trm_SRBdone does bcopy(trm_get_sense_buf(pACB pSRB) &pcsio->sense_data pcsio->sense_len). sense_len u_int8_t propagated verbatim from userland. Source slot 32 bytes DMA hard-bounded destination csio->sense_data 32 bytes. sense_len>32 bcopy reads past source DMA slot kernel-heap info leak AND writes past sense_data into rest of csio corrupting cdb_io msg_ptr tag_action tag_id init_id.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2522 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 6.7 KB ↓ raw
df252x_trm_rig.c β€” 18.2 KB view raw
trm.h β€” 34.2 KB view raw
Makefile β€” 125 B ↓ download
build.sh β€” 192 B view raw
run.sh β€” 280 B view raw
build.log β€” 5.7 KB view raw
run.log β€” 2.8 KB view raw
run.patched.log β€” 4.1 KB view raw
fix.diff β€” 416 B view raw
fix_build.log β€” 5.6 MB ↓ download
env.txt β€” 710 B view raw
verdict.json β€” 4.8 KB view raw

DF-2522 β€” PoC evidence pack (verify run, 2026-10-14)

Finding: sys/dev/disk/trm/trm.c β€” trm_SRBdone() auto-request-sense completion copies pcsio->sense_len bytes out of a 32-byte DMA sense slot into the 32-byte csio->sense_data field (trm.c:2529-2530). sense_len is a u_int8_t carried verbatim from the CAM CCB (user-settable via pass(4); no CAM/pass validation clamps it β€” grep sense_len scsi_pass.c cam_xpt.c finds no check), so any value 33..255 overflows both sides of the copy.

How it was verified

The audit guest has no Tekram DC395 hardware, but the driver ships as the production module /boot/kernel/trm.ko (same sources, same toolchain): kldload trm puts the real static handlers into the running kernel (nm: trm_Interrupt at file offset 0x1d40 β€” the static trm_SRBdone/trm_Disconnect/trm_Reselect are inlined into it).

The rig (df252x_trm_rig.c) contains zero driver code. It

  1. fabricates the post-attach ACB/DCB/SRB state exactly as the driver builds it (trm.h structs; SRB in the SRB_COMPLETED | AUTO_REQSENSE state trm_RequestSense() leaves it in);
  2. emulates the TRM-S1040 register file with a scratch page behind the kernel's own bus_space accessors (tag = X86_64_BUS_SPACE_MEM, bsh = kva; on amd64 these are plain volatile loads/stores β€” exactly what the driver sees on its memory-mapped BAR);
  3. provides the CCB as kmalloc(sizeof(union ccb) + 256) with sentinel bytes after the ccb (a real pass(4) CCB is exactly kmalloc(sizeof(union ccb)) β€” xpt_alloc_ccb, cam_xpt.c:4933);
  4. dispatches the running kernel's own trm_Interrupt() (address = kldstat base + nm offset, injected via sysctl), driving it through INT_DISCONNECT β†’ trm_Disconnect β†’ SRB_COMPLETED β†’ trm_SRBdone β†’ the AUTO_REQSENSE branch β†’ the bcopy.

What the run shows (stock kernel)

  • control (sense_len=32): clean completion, no byte out of place;
  • sense_len=255: the 255-byte bcopy
  • reads 223 bytes past the 32-byte DMA sense slot β€” with TagNumber=255 (last of 256 slots) that is 223 bytes past the whole sense_buffers array into adjacent kernel heap, and those bytes land in csio->sense_data where CAM_AUTOSNS_VALID hands them back to userland with the completed CCB (kernel-heap info leak),
  • writes 223 bytes past sense_data over live ccb_scsiio fields: sense_len, cdb_len, sglist_cnt, scsi_status (just set to CHECK_COND by trm.c:2528), sense_resid, resid, cdb_io (cdb_ptr pointer), msg_ptr (pointer), msg_len, tag_action, tag_id, init_id.
  • On this platform sizeof(union ccb)=720 > offsetof(sense_data)+255, so the write stays inside the CCB allocation (intra-object overflow of live fields incl. two pointers); it does not cross the heap chunk.
  • guest survives (the corrupted-CRB completion path is exercised with func_code=XPT_NOOP so xpt_done() is a no-op for the fabricated CCB).

Build / run (in guest, as root)

sh build.sh                      # builds df252x_trm_rig.ko
kldload trm                      # production Tekram driver module
sh run.sh                        # control(32) then DF-2522(255), guest stays up

Fix validation

fix.diff clamps the copy: imin(pcsio->sense_len, sizeof(pcsio->sense_data)). Validated by rebuilding the guest kernel with all three trm fixes applied (each scenario exercises only its own hunk) and re-running the same rig: sense_len=255 then produces the 32-byte control behavior β€” ABSENT - COPY WAS CLAMPED (0/223 bytes), all live fields intact, scsi_status=0x02 preserved.

VERDICT.md
↓ download raw

DF-2522 VERDICT β€” REPRODUCED (production handler, stock module)

Status: reproduced β€” the production trm_SRBdone() auto-sense bcopy (sys/dev/disk/trm/trm.c:2529-2530) copies csio->sense_len (a u_int8_t carried verbatim from the CAM CCB) bytes out of a 32-byte DMA sense slot into the 32-byte csio->sense_data field. With sense_len=255 the running kernel's own handler code read 223 bytes past the 32-byte slot β€” past the entire 256-slot sense_buffers array into adjacent kernel heap β€” and wrote them over the live tail of ccb_scsiio. Guest survived; both overflow directions proven byte-exact by pattern provenance. The one-line clamp fix eliminates every marker on a rebuilt module.

Rig (production code, not a re-implementation)

No Tekram DC395 HBA exists on the guest, but the driver ships as the production module /boot/kernel/trm.ko (identical sources: md5 /usr/src/sys/dev/disk/trm/trm.c = bf84dbcb… matches the audit tree). kldload trm puts its real static handlers into the running kernel β€” trm_SRBdone, trm_Disconnect, trm_Reselect are all inlined into trm_Interrupt (file offset 0x1d40 in nm). The KLD rig (df252x_trm_rig.c) contains zero driver logic; it fabricates the post-attach ACB/DCB/SRB/CCB state exactly as the driver builds it, maps the TRM-S1040 register file as a scratch page behind the kernel's own bus_space accessors (X86_64_BUS_SPACE_MEM β†’ plain volatile loads/stores, what the driver sees on its memory BAR), and dispatches the module's real trm_Interrupt() at kldstat base + nm offset (0xffffffff826001d40, stock). The interrupt choreography (SCSI_STATUS has SCSIINTERRUPT, SCSI_INTSTATUS = INT_DISCONNECT) drives the production path trm_Interrupt (trm.c:1323) β†’ trm_Disconnect (trm.c:2402-2404, SRB_COMPLETED) β†’ trm_SRBdone (trm.c:2511 AUTO_REQSENSE branch) β†’ the bcopy at trm.c:2529.

The CCB is presented as kmalloc(sizeof(union ccb) + 256) with sentinels β€” a real pass(4) CCB is exactly kmalloc(sizeof(union ccb)) (xpt_alloc_ccb, cam_xpt.c:4933).

Stock run (run.log)

  • Control sense_len=32 (SSD_FULL_SIZE): clean completion, sense bytes land in sense_data, nothing else moves β€” validates the fabricated state and the dispatch (all production side-effects present: pDCB->pActiveSRB cleared at trm.c:2402, SRB freed onto pACB->pFreeSRB, SgSenseTemp/Segment0/1 restores, ccb_h.status = CAM_SCSI_STATUS_ERROR|CAM_AUTOSNS_VALID).
  • sense_len=255:
  • dest bytes [32..255) = 0xA7 … CONFIRMED (223/223 bytes) β€” the 0xA7 pattern exists only past the 256-slot sense_buffers array (slot 255 = last of 256; its 32 bytes are the 0x51 pattern which lands in sense_data[0..31]). 223 adjacent-kernel-heap bytes were copied into the CCB. With CAM_AUTOSNS_VALID set (trm.c:2531-2532) these bytes go back to the CAM client β€” with a pass(4) CCB that is userland-visible kernel heap disclosure on every command.
  • clobbered LIVE ccb fields: sense_len=0xa7 cdb_len=0xa7 sglist_cnt=0xa7a7 scsi_status=0xa7 (just set to 0x02 by trm.c:2528, overwritten!) … cdb_io.cdb_ptr=0xa7a7…a7 msg_ptr=0xa7a7…a7 … β€” the OOB write side.
  • Layout facts printed at runtime: sizeof(struct scsi_sense_data)=32, sizeof(struct ccb_scsiio)=320, sizeof(union ccb)=720, offsetof(csio,sense_data)=236 β†’ the 255-byte write reaches offset 491, i.e. stays inside the 720-byte CCB allocation on this platform (intra-object overflow of live fields, including the msg_ptr and cdb_io.cdb_ptr pointers), and does not cross the heap chunk. The read side crosses the sense_buffers array end (TagNumber=255 β†’ reads [8160..8414] of the 8192-byte array).

Reachability / threat notes

sense_len is fully attacker-chosen in the CAM CCB (33..255); no clamp exists in pass(4) or xpt (grep sense_len in scsi_pass.c / cam_xpt.c finds no validation). In practice the trigger needs CAM command submission on a machine with the Tekram HBA β€” /dev/passN (root/operator) or in-kernel CAM clients. The DMA that fills the slot is SG-bounded to 32 bytes (trm.c:2837-2838), so the device cannot influence the length β€” only the CCB originator can. Filing severity (Medium) is consistent: privileged-local trigger, heap-disclosure primitive (223 bytes/heap layout per command) plus ccb field/pointer corruption.

Fix validation (fix.diff, run.patched.log, fix_build.log)

imin(pcsio->sense_len, sizeof(pcsio->sense_data)) β€” the copy clamps to 32. Validated by rebuilding the driver module from the patched source (same tree as fix.all.diff, which also carries the independent DF-2523/DF-2524 hunks; this scenario exercises only the bcopy hunk): make nativekernel KERNCONF=X86_64_GENERIC (full log fix_build.log) produced /usr/obj/…/dev/disk/trm/trm.ko (md5 ea902f81…, vs stock 7211861d…), loaded in place of the stock module, and the SAME rig re-dispatched against the patched module's own trm_Interrupt:

  • dest bytes [32..255) = 0xA7 … ABSENT - COPY WAS CLAMPED (0/223 bytes);
  • live fields intact: scsi_status=0x02 (CHECK_COND preserved), cdb_len/sglist_cnt/resid/cdb_ptr/msg_ptr/tag_* all zero (bzero state), sense_len=0xff (the user's own value, untouched);
  • the 32 legal bytes still copied correctly (sense_data[0..31] all 0x51).

Baseline bad behavior gone; control semantics preserved.

Things checked and killed

  • xpt_done() on the fabricated CCB: neutralized by func_code = XPT_NOOP (no XPT_FC_QUEUED bit β†’ xpt_done returns immediately, cam_xpt.c:4896; CAMDEBUG not compiled into this kernel β€” no cam_dflags symbol β€” so the CAM_DEBUG macro is a no-op). This is rig hygiene, not a driver finding.
  • Write crossing the CCB allocation: false on amd64 layout (491 < 720); documented as intra-object overflow β€” the finding text ("writes past sense_data into rest of csio corrupting cdb_io msg_ptr tag_action tag_id init_id") matches exactly.
  • REQUEST_SENSE CDB allocation-length write at trm.c:2844 (CmdBlock[4..5] = sense_len): noted; the DMA remains SG-bounded to 32 bytes (trm.c:2838), so it is not itself an overflow β€” clamping the bcopy is sufficient for this bug.

Bottom line

Real bug, reproduced through the running kernel's own driver code, both overflow directions byte-proven; one-line clamp fixes it, verified on a rebuilt module. Hardware gate: machines with Tekram DC395/TRM-S1040 HBAs.

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

Rebuilt the driver module from the patched source (make nativekernel KERNCONF=X86_64_GENERIC with fix applied; full log fix_build.log) and re-ran the identical rig against the patched module's own trm_Interrupt: sense_len=255 now copies exactly 32 bytes - 'ABSENT - COPY WAS CLAMPED (0/223 bytes)', all live ccb fields intact (scsi_status=0x02 CHECK_COND preserved, cdb_ptr/msg_ptr zero), and the legal sense bytes still delivered. Baseline bad behavior gone; control semantics unchanged.

run.patched.log lines 'ABSENT - COPY WAS CLAMPED (0/223 bytes)' and 'clobbered LIVE ccb fields: sense_len=0xff cdb_len=0x00 ... scsi_status=0x02 ... msg_ptr=0x0' vs stock run.log 'CONFIRMED (223/223 bytes)'; module md5s in env.txt (stock 7211861de4b5473e974ea7f26aae6e79 vs patched ea902f81add1d968180a761ea3a38956).
↓ fix.diffper-fix-DF-2522

Confirmed kernel references

Detail

Exploit chain

Not pursued beyond the demonstrated primitives: the trigger requires CAM command submission (pass(4) /dev/passN is root/operator) on a machine with the Tekram HBA, so a user->root chain adds nothing; the demonstrated primitives are (a) kernel-heap info leak of 223 bytes/command into the returned CCB and (b) corruption of live CCB fields incl. cdb_io.cdb_ptr and msg_ptr pointers.

Evidence (decisive lines)

run.log: 'CONFIRMED (223/223 bytes)' OOB-read marker + 'clobbered LIVE ccb fields: ... cdb_io.cdb_ptr=0xa7a7... msg_ptr=0xa7a7...' ; control run (sense_len=32) in same log shows zero corruption; run.patched.log: 'ABSENT - COPY WAS CLAMPED (0/223 bytes)', scsi_status=0x02 intact; fix.diff; fix_build.log (nativekernel, patched trm.ko md5 ea902f81 vs stock 7211861d).

PoC changes

No seed PoC existed (empty pack). Built from scratch: KLD rig with zero driver code that kldloads the production /boot/kernel/trm.ko, resolves its inlined trm_SRBdone via trm_Interrupt (kldstat base + nm offset 0x1d40, sysctl-injected), fabricates the driver state per trm.h, emulates the chip registers behind real bus_space MEM accessors, and dispatches.

Verified recommended fix

Clamp the auto-sense copy: bcopy(..., imin(pcsio->sense_len, sizeof(pcsio->sense_data))).

Verdict

Reproduced with the production handler: kldload'd the real /boot/kernel/trm.ko and dispatched its own trm_Interrupt() over a fabricated post-attach ACB/DCB/SRB (trm.h structs) with the TRM-S1040 register file emulated behind the kernel's bus_space accessors. An auto-request-sense completion with csio->sense_len=255 (user-controlled u_int8_t, no CAM/pass clamp) made the production bcopy at trm.c:2529-2530 read 223 bytes past the 32-byte DMA sense slot - past the entire 256-slot sense_buffers array into adjacent kernel heap, byte-proven by pattern provenance (0xA7 exists only beyond the array) - and write those 223 bytes over the live ccb_scsiio tail: sense_len, cdb_len, sglist_cnt, scsi_status (set to 0x02 by trm.c:2528 one instruction earlier, then overwritten), sense_resid, resid, cdb_io.cdb_ptr, msg_ptr, msg_len, tag_action, tag_id, init_id. On this platform sizeof(union ccb)=720 > offsetof(sense_data)+255, so the write stays inside the CCB allocation (intra-object overflow incl. two pointer fields); the read side hands 223 adjacent kernel-heap bytes back to the CAM client via CAM_AUTOSNS_VALID (userland-visible with pass(4)). Guest survives. One-line clamp (imin with sizeof(pcsio->sense_data)) validated on a rebuilt module: 0/223 overflow bytes, all fields intact, legal 32-byte copy preserved.