# DF-2273 — Unbounded strlen() on user-supplied cv_volid in chsetvoltag

**File:** `sys/bus/cam/scsi/scsi_ch.c:1300-1301`
**Claim:** `strlen(csvr->csvr_voltag.cv_volid)` on a possibly
non-NUL-terminated 33-byte field (`u_char cv_volid[CH_VOLTAG_MAXLEN+1]`,
sys/sys/chio.h:133-136) walks past the field; the memcpy is bounded by
`min(..., sizeof(ssvtp.vitf))` so only the scan over-reads.

## Verdict: REPRODUCED as described — bounded, no security impact beyond hardening

Verified on the harness kernel by interposing strlen inside the verbatim
driver TU (counting shim `dfh_strlen` that calls the real kernel strlen):

- NUL placed at cv_volid[k] -> scan length exactly k (0, 5, 32)
- **no NUL anywhere in the field, struct padding nonzero** -> scan length
  **36**: the scan walked past the 33-byte field through cv_serial
  (0x4242) and padding into adjacent bytes — the over-read of the claim
  (strlen_over33=1, strlen_max=36, voltag_baseline.log)
- **no retained over-read**: the fake transport captured
  ssvtp.vitf — exactly 32 data bytes (0x41..) + space padding,
  `svt_vitf_nonspace=32`, confirming `min(,32)` bounded the copy

## Containment discovered during verification

The DragonFly ioctl layer copyins the csvr into a kernel stack buffer
before chsetvoltag runs (sys/kern/sys_generic.c:685 -> :724), so the
scan walks KERNEL memory (struct + a few adjacent stack bytes until the
first zero), never user pages. The classic FreeBSD-style ceiling (walk
into an unmapped user page -> kernel page fault) is unreachable here:
the `boundary` test (csvr ends exactly at a page boundary, next page
PROT_NONE) completes SUCCESSFULLY with no fault — the syscall layer
copied only the struct itself.

## Fix (validated A/B)

`fix.diff`: strnlen bounded by the field size:

```diff
-	memcpy(ssvtp.vitf, csvr->csvr_voltag.cv_volid,
-	       min(strlen(csvr->csvr_voltag.cv_volid), sizeof(ssvtp.vitf)));
+	memcpy(ssvtp.vitf, csvr->csvr_voltag.cv_volid,
+	       min(strnlen(csvr->csvr_voltag.cv_volid,
+			   sizeof(csvr->csvr_voltag.cv_volid)),
+		   sizeof(ssvtp.vitf)));
```

Fixed kernel: strlen_calls=0, **strnlen_calls=5, strnlen_max=33** (scan
stops at the field end), retained copy still 32 bytes + spaces,
boundary case still clean (voltag_fixed.log).

## Reproduce

See ../DF-2270/rig/ (shared harness). `run_tests.sh` / `run_fixed.sh`
include the `vt` and `boundary` sub-tests; `ch_user vt none|far|boundary`.

## Files

- voltag_baseline.log / voltag_fixed.log — scan counters vuln vs fixed
- ch_user.c — the vt/boundary driver
- fix.diff, env.txt, verdict.json, manifest.json
