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)
PoC verification
Evidence pack
findings/poc/DF-2522 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- fabricates the post-attach ACB/DCB/SRB state exactly as the driver
builds it (
trm.hstructs; SRB in theSRB_COMPLETED | AUTO_REQSENSEstatetrm_RequestSense()leaves it in); - emulates the TRM-S1040 register file with a scratch page behind the
kernel's own
bus_spaceaccessors (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); - provides the CCB as
kmalloc(sizeof(union ccb) + 256)with sentinel bytes after the ccb (a real pass(4) CCB is exactlykmalloc(sizeof(union ccb))βxpt_alloc_ccb, cam_xpt.c:4933); - dispatches the running kernel's own
trm_Interrupt()(address =kldstatbase +nmoffset, injected via sysctl), driving it throughINT_DISCONNECTβtrm_DisconnectβSRB_COMPLETEDβtrm_SRBdoneβ theAUTO_REQSENSEbranch β 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 wholesense_buffersarray into adjacent kernel heap, and those bytes land incsio->sense_datawhereCAM_AUTOSNS_VALIDhands them back to userland with the completed CCB (kernel-heap info leak), - writes 223 bytes past
sense_dataover liveccb_scsiiofields: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_NOOPsoxpt_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.
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 insense_data, nothing else moves β validates the fabricated state and the dispatch (all production side-effects present:pDCB->pActiveSRBcleared at trm.c:2402, SRB freed ontopACB->pFreeSRB,SgSenseTemp/Segment0/1restores,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 insense_data[0..31]). 223 adjacent-kernel-heap bytes were copied into the CCB. WithCAM_AUTOSNS_VALIDset (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 themsg_ptrandcdb_io.cdb_ptrpointers), 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 byfunc_code = XPT_NOOP(noXPT_FC_QUEUEDbit β xpt_done returns immediately, cam_xpt.c:4896; CAMDEBUG not compiled into this kernel β nocam_dflagssymbol β 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
fixedRebuilt 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).
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.
No comments yet.