# 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.
