# DF-5332 VERDICT

## Status: REPRODUCED (deterministic, 6/6 runs: 3 marked + 3 unmarked) — fix validated

## Root cause (source-accurate)

`chgetelemstatus()` (sys/bus/cam/scsi/scsi_ch.c):

- **:1089** `data = (caddr_t)kmalloc(1024, M_DEVBUF, M_INTWAIT);` — first
  READ ELEMENT STATUS buffer, **no M_ZERO**.
- **:1129** `data = (caddr_t)kmalloc(size, M_DEVBUF, M_INTWAIT);` — second
  (descriptor) buffer, **no M_ZERO**.
- Neither the driver nor `cam_periph_runccb()` (cam_periph.c:960-969, status
  check only) looks at `csio->resid`. A SCSI device may complete READ
  ELEMENT STATUS **GOOD** while transferring fewer bytes than the CDB's
  allocation length — a short-but-good transfer is legal and leaves
  `resid > 0` with status CAM_REQ_CMP.
- The device therefore fully controls how much of each buffer is actually
  written. Everything beyond that is **stale kernel heap** (M_DEVBUF zone
  of size 1024 / `8+8+desclen*count`).
- **:1118** `desclen = scsi_2btoul(pg_hdr->edl)` — from buffer #1 (if the
  device short-transfers below byte 12 this is stale heap, feeding the
  `size` computation and the descriptor stride);
- **:1156-1183** `avail = scsi_2btoul(st_hdr->count)` (device), then
  `copy_element_status(softc, pg_hdr->flags, desc, ces)` walks `avail`
  descriptors at `desclen` stride. `pg_hdr->flags` (data[9]) comes from
  the **stale tail** of buffer #2 in the short case; if it has
  READ_ELEMENT_STATUS_PVOLTAG/AVOLTAG (0x80/0x40) set,
  `copy_voltag()` additionally copies 32+2 stale bytes per element.
- **:1186** `copyout(user_data, cesr->cesr_element_status, avail * sizeof(...))`
  hands all of it to userland.

Distinct from the known findings: DF-1028/DF-2271 are **out-of-bounds**
reads (desclen/avail walking *past* the allocation); this finding is the
**in-bounds uninitialized-memory disclosure** (missing `M_ZERO` + missing
resid validation) — the DF-5272 (CDIOCREADSUBCHANNEL) class in the ch
driver.

## How it was reproduced (guest, EVIL = GENERIC + `device ch`)

Rig: `evilcd3.ko` (fake T_CHANGER at target 1, `/dev/ch0`; mode page 0x1d
reports 1 picker / 8 slots @ addr 256). `debug.evilcd.resmode=1` makes the
sizing RES (count==1) answer normally (edl=16) and every real RES
(count>1) transfer **only the 8-byte status header** with
`avail = cesr_element_count` inside it, completing CAM_REQ_CMP with
resid = dxfer_len - 8 — byte-for-byte what a hostile real changer does on
the wire.

PoC `poc_chres 3 1 4`: CHIOGSTATUS(chet=CHET_ST, base=0, count=4);
`debug.evilcd.markres=4` first stamps 96 freed M_DEVBUF chunks of exactly
`8+8+16*4 = 80` bytes with marker bytes 0x21..0x2F (chosen WITHOUT the
0x40/0x80 bits so the stale page-header flags cannot set PVOLTAG/AVOLTAG
and drag in the separately-filed OOB-walk class).

### Result (marked runs, run.log)
Every returned `struct changer_element_status` contained the 0x26 marker:
```
-- element 0 (92 bytes, 7 non-zero): int_addr=0x2626 flags=0x126
   sense=0x26/0x26 scsi_id=38 ...
[***] LEAK CONFIRMED: 84 non-zero bytes across 3 runs
```
28 non-zero bytes per run, identical across runs — the driver's 80-byte
kmalloc deterministically reused the stamped chunk.

### Result (unmarked runs, run.2.log — natural stale heap)
Without any marking, real stale kernel heap was disclosed:
```
int_addr=0xa0cf flags=0x3f9 sense=0x01/0xf8 scsi_id=255 lun=7
int_addr=0x805a flags=0x3fb ...
[***] LEAK CONFIRMED: 51 non-zero bytes across 3 runs
```
(`0xa0cf`, `0x805a`, `f9 03 01 f8 ff` — residue of prior kernel
allocations in the 80-byte M_DEVBUF zone; high bytes consistent with
freed-slab metadata/pointer fragments.)

Console corroboration (dmesg):
```
[EVILCH] RES probe: sea=256 count=1 full 32B (edl=16)
[EVILCH] RES evil short: sea=256 count=4 dxfer=80 SENT=8B only
```

## Impact

Kernel heap info leak to the CHIOGSTATUS caller. `/dev/chN` is 0600
root:operator (make_dev at scsi_ch.c:364-366), so the direct reader is
root/operator — but the *content* is whatever previously lived in those
M_DEVBUF chunks (any other subsystem's data: file path fragments, socket
buffers, crypto material...). Combined with an evil/compromised changer
(USB-attached SCSI changers exist), this is a kernel-memory disclosure
primitive against whoever queries element status. Class/severity aligned
with DF-5272 (Medium, kernleak). No write primitive, no user→root path —
leak only.

## Fix validation (done)

`fix.diff` adds `M_ZERO` to both allocations. Guest /usr/src patched,
kernel rebuilt (EVIL), reinstalled, rebooted; **exact same PoC rerun**:
all returned structs zero — `run` prints `all returned bytes zero` /
`[---]` (exit 3), zero non-zero bytes across marked and unmarked runs
(`fixedA.log`), while the rig still reports
`RES evil short: ... SENT=8B only` — proving the device behaviour was
unchanged and only the kernel-side disclosure was removed.

Baseline (vulnerable): 84 (marked) + 51 (unmarked) non-zero bytes.
Patched: 0 non-zero bytes in both modes.

## Guest
DragonFly dfbsd 6.5-DEVELOPMENT #0: Fri Oct 2 22:55:55 UTC 2026
root@dfbsd:/usr/obj/usr/src/sys/EVIL x86_64 (GENERIC + `device ch`);
gcc (DragonFly) 8.3; evilcd3.ko rig.

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