β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2273

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2273 Β· 11 files
FileTypeDescriptionSize
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, 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:

-   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
VERDICT.md
↓ download raw

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_testable
↓ fix.diffper-fix-DF-2273

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