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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5710 Β· 17 files
FileTypeDescriptionSize
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.c under 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)
VERDICT.md
↓ download raw

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 strncpy over 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), exactly n bytes are written ([0,k) = src, [k,n) = NUL-pad), canary beyond n untouched (part B). Hand-trace of strncpy.c:49-56: NUL at index k leaves n = 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) β‰₯ 76 fills budget1 (80-2-2); index lands 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), index lands at 78, '/' at 78, cursor = 79 β†’ budget3 = βˆ’1 β†’ strncpy(f+79, ss_name, SIZE_MAX) (smbfs_vfsops.c:176); share names up to SMB_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

  1. fix.diff (pe - pc > 2 guards evaluated at the right points) applies clean to guest /usr/src (patch -p1 --dry-run: Hunk #1 succeeded at 167).
  2. Guest builds sys/vfs/smbfs β†’ smbfs.ko with the fix under -Werror.
  3. harness_smbfs_fixed.c (same rig, guarded caller): every case β€” including both βˆ’1 routes β€” clean exit with correctly truncated, NUL-terminated f_mntfromname. Baseline behavior for benign inputs unchanged ('//joe@fs01/pub').
  4. Guest source reverted after validation; guest left clean (vm.sh status up).

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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5710

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 ; (2) budgets must be computed AT the strncpy call point (after pc++), exactly as the in-tree expression evaluates -- precomputing before the separator write hid the -1.

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.