Unbounded strlen() on user-supplied cv_volid in chsetvoltag can read past the 33-byte field
Summary
chsetvoltag calls strlen(csvr->csvr_voltag.cv_volid) on user-supplied 33-byte cv_volid array without first guaranteeing NUL termination. If user fills all 33 bytes with non-NUL characters strlen walks into cv_serial and padding scanning until it finds a NUL byte. Result clamped by min(...sizeof(ssvtp.vitf)==32) so memcpy is bounded - only strlen scan over-reads and over-read bytes are not retained. Hardening item - strnlen is the right primitive. [VERIFIED pass2: reproduced as described - interposed strlen counter shows scan 36 bytes past the 33-byte field while retained copy stays bounded at 32; copyin containment keeps the walk on the kernel stack copy, page-boundary PROT_NONE experiment clean; strnlen fix validated]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2273 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 2.7 KB | β raw | |
| verdict.json | β | 2.5 KB | view raw | |
| manifest.json | β | 861 B | view raw | |
| fix.diff | β | 446 B | view raw | |
| env.txt | β | 647 B | view raw | |
| voltag_baseline.log | β | 534 B | view raw | |
| voltag_fixed.log | β | 497 B | view raw | |
| ch_user.c | β | 8.9 KB | view raw | |
| build.log | β | 2.9 KB | view raw | |
| run.log | β | 534 B | view raw |
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, confirmingmin(,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:
- 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
DF-2273 β VERDICT
Status: REPRODUCED (as the bounded scan over-read described); impact none
Mechanism (line-accurate)
chsetvoltag (scsi_ch.c:1238) receives the csvr and at :1300-1301 executes
memcpy(ssvtp.vitf, csvr->csvr_voltag.cv_volid, min(strlen(csvr->csvr_voltag.cv_volid), sizeof(ssvtp.vitf)));
cv_volid is u_char[CH_VOLTAG_MAXLEN+1] = [33] (sys/sys/chio.h:133-136); vitf is u_int8_t[32] (scsi_ch.h:299-305). Nothing guarantees a NUL inside the 33 bytes, so strlen scans past the field on a non-terminated request.
Mechanical verification
strlen interposed (counting shim -> real strlen) inside the verbatim driver TU on the harness kernel; userland driver places NUL at chosen offsets or none:
- nulpos 0/5/32 -> scan 0/5/32 (correct stop at the NUL)
- struct fully nonzero past the field -> scan 36: walked cv_volid[33] + cv_serial (2) + padding + 1 byte beyond the copied struct into kernel stack (strlen_over33=1, strlen_max=36)
- ssvtp.vitf as seen by the transport: 32 data bytes + space padding (svt_vitf_nonspace=32) β the retained copy is bounded by min(,32); NO retained over-read (exactly what the finding claims)
Impact ceiling β lower than the classic shape
DragonFly marshals _IOW ioctl payloads through a one-shot copyin into a kernel stack buffer (sys/kern/sys_generic.c:685 copyin, :724 fo_ioctl with the kernel buffer). The scan therefore runs over kernel stack memory adjacent to the copy, stopping at the first zero byte; it cannot walk user pages, cannot reach unmapped user VA, and cannot fault. The boundary experiment proves it end to end: csvr placed so the field ends exactly at a page boundary with the next page PROT_NONE -> ioctl SUCCEEDS (the syscall-layer copyin touches only the struct itself); no kernel-mode walk of user memory exists in this path.
Net effect: a few bytes of adjacent kernel stack are scanned (not disclosed anywhere β the memcpy copy is bounded and vitf is space-padded before the copy). Info severity stands; impact none.
Fix validation
strnlen with the field-size bound (fix.diff). Fixed kernel counters: strlen_calls=0, strnlen_calls=5, strnlen_max=33 β the scan now stops at the field end in every case; retained copy unchanged (32 bytes); boundary case clean. fix_status: fixed.
Files
voltag_baseline.log, voltag_fixed.log, ch_user.c, fix.diff, env.txt, verdict.json, manifest.json; shared rig ../DF-2270/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.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
voltag_baseline.log (strlen_over33=1 strlen_max=36 svt_vitf_nonspace=32), voltag_fixed.log (strnlen_calls=5 strnlen_max=33 strlen_calls=0), boundary lines in both logs (clean SUCCESS, no fault), rig df_post.c dfh_strlen/dfh_strnlen instrumentation
PoC changes
No seed existed; wrote ch_user.c vt/boundary subcommands plus the dfh_strlen/dfh_strnlen counting interposition in the harness postamble.
Verified recommended fix
Bound the scan: replace strlen with strnlen(cv_volid, sizeof(cv_volid)) in the min() at scsi_ch.c:1300 (fix.diff)
Verdict
The unbounded strlen at scsi_ch.c:1300 reproduces exactly as claimed: with no NUL in the 33-byte cv_volid the scan ran 36 bytes, past the field through cv_serial and padding into adjacent bytes of the kernel stack copy (interposed counting shim, strlen_over33=1 strlen_max=36), while the retained ssvtp.vitf copy stayed bounded at 32 data bytes plus space padding (svt_vitf_nonspace=32) β a scan-only over-read with nothing retained. Containment established: DragonFlys ioctl layer copyins the csvr once into a kernel stack buffer (sys_generic.c:685 -> :724), so the walk cannot reach user memory or unmapped pages; the page-boundary/PROT_NONE experiment completed with no fault. The strnlen bound (fix.diff) caps the scan at 33 on the fixed kernel (strnlen_max=33, strlen_calls=0) with identical retained output. Info severity stands; no security impact beyond hardening.
No comments yet.