smbfs_mount: strncpy budget 'pe - pc - 2' can be -1, wrapping to (size_t)-1 β unbounded kernel heap copy during smbfs mount
Summary
The guard 'pc < pe-1' is evaluated before the separator write + pc++ while the budget 'pe - pc - 2' is computed after them: at cursor offset 79 the budget is -1 (ptrdiff) which wraps to SIZE_MAX in strncpy's size_t param - strncpy copies the source then zero-pads WITHOUT BOUND from f_mntfromname[79] through the rest of struct mount and adjacent kernel heap. Two user-controlled routes: username >=76 chars (budget2) and username==75 (budget3 on the share name); usernames up to SMB_MAXUSERNAMELEN=128 are legal nsmb ioctl input. Guard-page harness replicating the call chain verbatim: budget=-1 printed at the call point then SIGSEGV across the guard, both routes; 1MB-runway case zeroes everything to the far guard. Privileged trigger (root mount or permissive vfs.usermount+/dev/nsmb) - legacy bucket. Room-check-at-budget-time fix validated (smbfs.ko builds -Werror, fixed caller survives all hostile cases, benign output byte-identical). Implementation itself proven clean (libc-identical, exact n writes, guard rig).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5710 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness_smbfs.c | β | 5.7 KB | view raw | |
| harness_smbfs_fixed.c | β | 6.0 KB | view raw | |
| harness_strncpy.c | β | 6.9 KB | view raw | |
| harness_netmap_clamp.c | β | 1.7 KB | view raw | |
| shim/sys/libkern.h | β | 284 B | view raw | |
| build.sh | β | 174 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 571 B | view raw | |
| run.log | β | 1.9 KB | view raw | |
| run_baseline.log | β | 740 B | view raw | |
| fix.diff | β | 990 B | view raw | |
| fix_build.log | β | 1.0 KB | view raw | |
| fix_run.log | β | 2.5 KB | view raw | |
| env.txt | β | 397 B | view raw | |
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 5.8 KB | β raw | |
| verdict.json | β | 5.3 KB | view raw |
DF-5710 β smbfs_mount: pe - pc - 2 can be β1 β strncpy(dst, src, (size_t)-1) β unbounded kernel heap copy
- File (audit target):
sys/libkern/strncpy.c(pass 2, consumer census) - Defect location:
sys/vfs/smbfs/smbfs_vfsops.c:170-176(smbfs_mount) - Severity: Medium (kernel memory corruption; trigger requires mount-time privilege + an established SMB session β Dillon legacy bucket)
- Confidence: certain (arithmetic verified against real struct sizes;
reproduced with the guest kernel's own
strncpy.cunder guard pages)
Reproduce
On the audit QEMU guest (DF 6.5-DEVELOPMENT), with the rig transferred to
/tmp/rig (see build.sh; the kernel file is compiled unmodified from
/usr/src/sys/libkern/strncpy.c):
./build.sh && ./run.sh
Expected (decisive lines):
[case1 user-route] after user+'@': pc offset=143 budget2=-1 -> result: SIGSEGV (write crossed guard page) [user-route guard] [case2 share-route] after srv+'/': pc offset=143 budget3=-1 (share='pub') -> result: SIGSEGV (write crossed guard page) [share-route guard]
and, with the fixed caller (./harness_smbfs_fixed), the same inputs:
-> result: clean exit(0) (every case, f_mntfromname NUL-terminated)
What the rig contains
| file | purpose |
|---|---|
harness_strncpy.c |
the audited file itself: differential vs libc (matrix + 100k fuzz), pad-count math (exactly-n canary proof), guard-page src/dst bounds. All pass β the implementation is clean; the bug is at the caller. |
harness_smbfs.c |
verbatim caller code from smbfs_vfsops.c:165-178 with real sizes (MNAMELEN=80), guard-paged mount-struct analogue, 4 cases |
harness_smbfs_fixed.c |
same rig with the pe - pc > 2 guards from fix.diff β fix validation |
harness_netmap_clamp.c |
DF-5711 evidence (shared rig) |
shim/sys/libkern.h |
minimal shim so the kernel file compiles unmodified |
fix.diff |
the smbfs fix (applies clean to guest /usr/src; module builds with -Werror) |
fix_netmap.diff |
the DF-5711 fix (applies clean) |
DF-5710 VERDICT β reproduced (guard-page harness; caller-side unbounded copy)
What was audited and what was found where
Pass-2 audit of sys/libkern/strncpy.c (59 LOC). The implementation itself is
provably correct β this is the classic Torek code and the rig proves it:
- (a) semantics differential β byte-exact image + return value vs libc
strncpyover the full(n, srclen) β [0,64]Β²matrix plus 100,000 random cases with embedded NULs: identical (run.log, part A). - (d) pad-count math β for every
(n, srclen), exactlynbytes are written ([0,k)= src,[k,n)= NUL-pad), canary beyondnuntouched (part B). Hand-trace ofstrncpy.c:49-56: NUL at index k leavesn= Nβk at the pad loop, which writes Nβkβ1 zeros β exactly N total. - (c) guard-page differential β src guard: NUL inside window + nβ«window
β no fault (pad loop never re-reads src,
strncpy.c:52-53); no NUL and n == window β no fault (reads exactly n). dst guard: n == space β no fault (writes exactly n); n == space+1 β SIGSEGV (bound is exactly n; callers must size for n). All as specified (parts C0/C1/D0/D1/D2).
So the audited file is clean; per the pass-2 checklist the hunt moved to the consumer census (β150 in-tree call sites). One caller is genuinely broken:
The bug (caller): sys/vfs/smbfs/smbfs_vfsops.c:170-176
pc = mp->mnt_stat.f_mntfromname; /* MNAMELEN == 80 */
pe = pc + sizeof(mp->mnt_stat.f_mntfromname);
bzero(pc, MNAMELEN);
*pc++ = '/'; *pc++ = '/';
pc = index(strncpy(pc, vcp->vc_username, pe - pc - 2), 0); /* :170 */
if (pc < pe-1) {
*(pc++) = '@';
pc = index(strncpy(pc, vcp->vc_srvname, pe - pc - 2), 0); /* :173 */
if (pc < pe - 1) {
*(pc++) = '/';
strncpy(pc, ssp->ss_name, pe - pc - 2); /* :176 */
}
}
The guards pc < pe-1 are evaluated before the separator write and the
pc++ that follows; the budget pe - pc - 2 is computed at the call,
after the increment. When the cursor reaches pe-1 (offset 79 of 80), the
budget is 80 - 79 - 2 = -1, a ptrdiff_t passed to strncpy's size_t
parameter β (size_t)-1 β unbounded copy + zero-fill from
f_mntfromname[79] through the rest of struct mount and adjacent kernel
heap. Two concrete routes (username allowed up to SMB_MAXUSERNAMELEN=128,
smb_dev.h:76 ioc_user[SMB_MAXUSERNAMELEN+1], i.e. 76+ chars is legal input):
- user route:
strlen(vc_username) β₯ 76fills budget1 (80-2-2);indexlands at offset 78;'@'written at 78; cursor = 79 β budget2 = β1 βstrncpy(f+79, vc_srvname, SIZE_MAX)(smbfs_vfsops.c:173). - share route:
strlen(vc_username) == 75β budget2 == 0 (no-op),indexlands at 78,'/'at 78, cursor = 79 β budget3 = β1 βstrncpy(f+79, ss_name, SIZE_MAX)(smbfs_vfsops.c:176); share names up toSMB_MAXSHARENAMELEN=128.
In-kernel effect: struct mount (kmalloc'd, M_MOUNT) is overrun from
mnt_stat.f_mntfromname onward β the copy first writes the source string
(β€15/128 attacker-chosen bytes for srvname/share route), then zero-pads
without bound: guaranteed corruption of struct mount fields and adjacent
slab objects β panic at best, deliberate heap zeroing at worst. Not a
controlled-write primitive, but a reliable, attacker-shaped DoS/corruption.
Reproduction (guest, DF 6.5-DEVELOPMENT, cc 8.3)
harness_smbfs.c replicates smbfs_vfsops.c:165-178 verbatim (indexβstrchr),
with the guest kernel's own strncpy.c compiled unmodified, a
mount-analogue struct placed against a guard page:
- case0 benign (
joe@fs01/pub): budgets 76/72/67, clean,'//joe@fs01/pub'. - case1
76*'u': budget2 = β1 printed at the call point β SIGSEGV crossing the guard page. - case2
75*'u': budget3 = β1 on the share strncpy β SIGSEGV. - case3 (1 MB runway): the pad loop zeroes the whole runway and still dies at the far guard β visualization of the kernel-heap zero-fill.
An in-guest kernel trigger (real mount_smbfs) was not attempted: reaching
smbfs_mount requires an established netsmb VC+SS (live SMB session) β
root-only setup, and the outcome (unbounded kernel copy) is exactly what the
harness demonstrates with the same arithmetic and the same strncpy.
Fix validation
fix.diff(pe - pc > 2guards evaluated at the right points) applies clean to guest/usr/src(patch -p1 --dry-run: Hunk #1 succeeded at 167).- Guest builds
sys/vfs/smbfsβsmbfs.kowith the fix under-Werror. harness_smbfs_fixed.c(same rig, guarded caller): every case β including both β1 routes β clean exit with correctly truncated, NUL-terminatedf_mntfromname. Baseline behavior for benign inputs unchanged ('//joe@fs01/pub').- Guest source reverted after validation; guest left clean (
vm.sh statusup).
Status classification
reproduced / impact dos-to-corruption (kernel heap overrun; not shaped
into a uid0 chain because the trigger is mount-privileged β Dillon legacy
bucket sys/vfs/smbfs/), confidence certain. guest_dirty: 0.
kernel_refs
- sys/vfs/smbfs/smbfs_vfsops.c:165-178 (defect)
- sys/netproto/smb/smb_dev.h:76,288-291 (ioc_user 129 bytes; SMB_MAXUSERNAMELEN 128)
- sys/netproto/smb/smb_usr.c:87 (spec->username = dp->ioc_user)
- sys/netproto/smb/smb_conn.c:479-483 (smb_strdup into vcp->vc_username)
- sys/sys/mount.h:93,115 (MNAMELEN 80; f_mntfromname[MNAMELEN])
- sys/libkern/strncpy.c:43-58 (the primitive β correct per 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
fixedfix.diff (pe - pc > 2 guards) applies clean to guest /usr/src (Hunk #1 succeeded at 167); guest builds sys/vfs/smbfs -> smbfs.ko with the fix under -Werror (make rc=0); fixed-caller harness reruns both -1 routes and the benign case: all clean exits with correctly truncated NUL-terminated f_mntfromname, and benign output byte-identical to baseline ('//joe@fs01/pub'). Full fixed-kernel boot not performed: smbfs is a KLD and reaching the vulnerable line in-kernel additionally requires a live SMB session; module compile + caller-level rerun is the decisive evidence for this defect. Guest source reverted after validation.
fix_build.log (apply + make rc=0 + smbfs.ko + SOURCE-REVERTED); fix_run.log (fixed harness: all cases clean exit); run.log (baseline: budget=-1 + SIGSEGV on the same inputs).
Confirmed kernel references
Detail
Exploit chain
Trigger requires mount privilege: vfs.usermount=0 and /dev/nsmb absent on the guest, and reaching smbfs_mount needs an established netsmb VC/SS (live SMB session) -- so no unprivileged->uid0 chain exists; that is a documented hard blocker, not an omitted step. Primitive characterized: unbounded strncpy from f_mntfromname[79] -- up to 15 (srvname) / 128 (share name) attacker-chosen bytes, then uncontrolled zero-fill until the kernel faults. No length control => even for the privileged triggerer this is reliable kernel-heap corruption/panic (crash/DoS class), not a shapeable overwrite primitive.
Evidence (decisive lines)
run.log: '[case1 user-route] after user+'@': pc offset=143 budget2=-1' + '-> result: SIGSEGV (write crossed guard page)'; '[case2 share-route] ... budget3=-1 (share=pub)' + SIGSEGV; fix_run.log: same inputs on the fixed caller all 'clean exit(0)' with NUL-terminated output; fix_build.log: patch applies (Hunk #1 succeeded at 167), smbfs.ko builds under -Werror, make rc=0; run_baseline.log: the audited strncpy implementation itself passes all differential/guard parts.
PoC changes
Seed PoC replaced entirely: harness compiles the guest kernel strncpy.c unmodified (-Dstrncpy=kstrncpy + 3-line sys/libkern.h shim), replicates smbfs_vfsops.c:165-178 verbatim with real sizes (MNAMELEN=80) against mmap'd guard pages. Two rig iterations were needed: (1) missing
Verified recommended fix
smbfs_vfsops.c: guard every budget with 'pe - pc > 2' evaluated at the call point so 'pe - pc - 2' can never be negative (fix.diff).
Verdict
Pass-2 consumer-census finding from the sys/libkern/strncpy.c audit: the implementation itself is proven correct (libc-identical differential, exact-n pad-count, guard-page read/write bounds -- run_baseline.log), but the smbfs_mount caller at sys/vfs/smbfs/smbfs_vfsops.c:170-176 computes its strncpy budget as 'pe - pc - 2' AFTER a pc++ that the 'pc < pe-1' guard did not account for; when the name walk reaches offset 79 of the 80-byte mnt_stat.f_mntfromname the budget is -1, which wraps to (size_t)-1 and strncpy copies/pads unboundedly from f_mntfromname through the rest of struct mount and adjacent kernel heap. Reproduced on the DF 6.5 guest with the kernel's own strncpy compiled unmodified: budget=-1 printed at the call point followed by SIGSEGV across a guard page on both the username route (>=76-char SMB username, allowed by SMB_MAXUSERNAMELEN=128 at smb_dev.h:76) and the share-name route (75-char username), plus a 1MB-runway case visualizing the unbounded zero-fill.
No comments yet.