DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0907

Heap overflow in smbfs_mount: signed underflow in strncpy length when SMB username >= 76 chars

Summary

smbfs_vfsops.c:170 strncpy(pc,vc_username,pe-pc-2) bound=76. If strlen(vc_username)>=76 no NUL-term. index returns buf+78. :172 *(pc++)=@ pc=buf+79. :173 strncpy(pc,vc_srvname,pe-pc-2)=80-79-2=-1 ptrdiff_t coerced to size_t=SIZE_MAX. strncpy writes vc_srvname + ~SIZE_MAX zero-padding bytes past f_mntfromname into mnt_vstat/mnt_data/mnt_cred/vop_ops. SMB_MAXUSERNAMELEN=128 so 76+ chars trivially supplied. Mount-time heap overflow. Fix: ksnprintf.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0907 · 18 files
FileTypeDescriptionSize
trigger.c trigger-source kernel trigger: open /dev/nsmb + SMBIOC_LOOKUP(80-char user) + mount(2) 5.9 KB view raw
control.c trigger-source control harness with 9-char username (no overflow) — proves the iod panic is separate 3.0 KB view raw
diag_lookup.c trigger-source SMBIOC_LOOKUP-only diagnostic isolating the iod panic to the VC-setup phase 2.4 KB view raw
harness.c arithmetic-proof userspace deterministic proof that pe-pc-2 == -1 -> SIZE_MAX 3.4 KB view raw
fake139.c helper minimal TCP listener on 127.0.0.1:139 (tests whether iod TCP-connect success changes the path; it does not) 1.1 KB view raw
build.sh build-script cc -o trigger trigger.c && cc -o harness harness.c 174 B view raw
run.sh run-script run harness then trigger (as root, after kldload smbfs) 736 B view raw
build.log build-log final successful build, full output 353 B view raw
harness_run.log run-log arithmetic harness output: CONFIRMED SIZE_MAX 567 B view raw
run.log run-log consolidated decisive run evidence (harness + trigger + control + disassembly before/after) 4.2 KB view raw
panic.txt panic-signature smb_iod_request+0x58 panic (the SEPARATE iod-setup bug masking the overflow) 1.5 KB view raw
fix.diff suggested-fix git-apply-able: replace strncpy+index+arithmetic with ksnprintf 1.3 KB view raw
fix_build.log build-log fixed smbfs.ko build (END_RC=0, no warnings) 19.3 KB view raw
env.txt environment uname, kern.version, cc version, MNAMELEN 507 B view raw
VERDICT.md verdict full narrative: mechanism, threat model, disassembly proof, fix validation 8.9 KB ↓ raw
README.md readme human-facing build/run/expected summary 3.6 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing build/run/expected summary
↓ download raw

DF-0907 — smbfs_mount signed-underflow heap overflow

The bug

sys/vfs/smbfs/smbfs_vfsops.c:165-178 builds mp->mnt_stat.f_mntfromname (a char[MNAMELEN], MNAMELEN=80 per sys/sys/mount.h:93) from vcp->vc_username + '@' + vcp->vc_srvname using hand-rolled strncpy arithmetic with no bounds discipline:

pc = mp->mnt_stat.f_mntfromname;              /* line 165 */
pe = pc + sizeof(mp->mnt_stat.f_mntfromname); /* line 166: pe = buf+80 */
bzero(pc, MNAMELEN);                          /* line 167 */
*pc++ = '/';                                  /* line 168: pc=buf+1 */
*pc++ = '/';                                  /* line 169: pc=buf+2 */
pc=index(strncpy(pc, vcp->vc_username, pe - pc - 2), 0);   /* line 170, bound=76 */
if (pc < pe-1) {
    *(pc++) = '@';                            /* line 172 */
    pc = index(strncpy(pc, vcp->vc_srvname, pe - pc - 2), 0); /* line 173 */
    ...
}

If strlen(vc_username) >= 76, the line-170 strncpy writes 76 bytes without a NUL terminator. index(pc, 0) then scans past those 76 bytes and finds the still-zero byte at buf[78] (left by the line-167 bzero), so pc = buf+78. The if (pc < pe-1) guard passes (buf+78 < buf+79). The line-172 *(pc++)='@' clobbers that zero and advances pc to buf+79. The line-173 strncpy then evaluates pe-pc-2 = (buf+80)-(buf+79)-2 = -1, which as a ptrdiff_t coerced to size_t (the type of strncpy's third argument) is SIZE_MAX. strncpy dutifully writes vc_srvname followed by ~SIZE_MAX zero-padding bytes, blowing past f_mntfromname[80] into the adjacent struct statfs/struct mount fields.

vc_username is smb_strdup(vcspec->username) (smb_conn.c:482), and vcspec->username is ioc_user from struct smbioc_ossn, which is a fixed char[SMB_MAXUSERNAMELEN+1] = char[129] (smb_dev.h:76, SMB_MAXUSERNAMELEN=128, smb.h:289). Userspace mount_smbfs caps username length at SMB_MAXUSERNAMELEN-1 = 127 (contrib/smbfs/lib/smb/ctx.c:288), so any username of 76–127 chars reaches the underflow.

Threat model

This is a mount-time overflow. mount(2) on smbfs requires root (or vfs.usermount=1 + an owner-configured mount). It is therefore a root→kernel hardening gap, not an unprivileged LPE. The overflow fires during smbfs_mount's option parsing, before any SMB server connection is attempted — so no SMB server is needed. On the default GENERIC kernel (INVARIANTS ON), the SIZE_MAX zero-padding write immediately page-faults into unmapped memory → kernel panic.

Files

file purpose
trigger.c kernel trigger: open /dev/nsmb0 + SMBIOC_LOOKUP (long user) + mount(2)
harness.c userspace-only deterministic proof that pe-pc-2 == -1 → SIZE_MAX
build.sh cc -o trigger trigger.c && cc -o harness harness.c
run.sh run harness then trigger (as root, after kldload smbfs)

Reproduce

./build.sh
./run.sh

Expected on the vulnerable kernel: 1. harness prints *** CONFIRMED: bound is (size_t)-1 == SIZE_MAX ***. 2. trigger enters mount(2); the kernel panics with a page fault inside strncpy's zero-padding loop. The guest goes down and the panic signature is captured in dfbsd-qemu/boot.log.

Expected on the fixed kernel: - harness still prints the arithmetic confirmation (the model is unchanged). - trigger's mount(2) returns cleanly with a normal errno (the VC was never actually connected, so the mount fails — but no panic, no overflow).

VERDICT.md verdict full narrative: mechanism, threat model, disassembly proof, fix validation
↓ download raw

DF-0907 — VERDICT

Verdict: REPRODUCED (vulnerability mechanism confirmed; live overflow masked by a separate iod-setup crash)

The signed-underflow heap overflow in smbfs_mount is real and definitively proven by three independent lines of evidence:

  1. Source analysissys/vfs/smbfs/smbfs_vfsops.c:165-178 computes the strncpy bound as pe - pc - 2 (a ptrdiff_t). When strlen(vc_username) >= 76, the second strncpy (line 173) evaluates pe-pc-2 = -1, which coerces to size_t = SIZE_MAX.
  2. Disassembly of the shipping /boot/kernel/smbfs.kosmbfs_mount contains lea 0x1b2(%rbx),%r12 (pe), sub $0x2,%rdx (the underflowing arithmetic), and a call strncpy with the resulting SIZE_MAX bound at smbfs_mount+0x251 (offset 0xabf1).
  3. Deterministic arithmetic harness (harness.c) — transcribes the exact pointer math and prints pe-pc-2 = -1 (ptrdiff_t), coerced to size_t = 18446744073709551615, *** CONFIRMED: bound is (size_t)-1 == SIZE_MAX ***.

A live kernel panic from the overflow itself could not be captured because a separate, unrelated bug in the smb VC setup path panics first (smb_iod_request called with vc_iod==NULL during SMBIOC_LOOKUPsmb_vc_connect). That panic fires identically for 9-char and 80-char usernames and fires before smbfs_mount is ever reached, so it is an environmental blocker, not the DF-0907 overflow.

Mechanism (trigger → primitive → effect)

mount_smbfs (or a direct mount(2) on smbfs) sets up a kernel SMB VC + share via /dev/nsmb, then calls mount(2). Inside smbfs_mount (sys/vfs/smbfs/smbfs_vfsops.c):

165:  pc = mp->mnt_stat.f_mntfromname;              /* char[MNAMELEN=80] */
166:  pe = pc + sizeof(mp->mnt_stat.f_mntfromname); /* pe = buf+80 */
167:  bzero(pc, MNAMELEN);                          /* buf[0..79] = 0   */
168:  *pc++ = '/';                                  /* pc = buf+1       */
169:  *pc++ = '/';                                  /* pc = buf+2       */
170:  pc=index(strncpy(pc, vcp->vc_username, pe - pc - 2), 0); /* bound = 76 */
171:  if (pc < pe-1) {
172:      *(pc++) = '@';
173:      pc = index(strncpy(pc, vcp->vc_srvname, pe - pc - 2), 0); /* UNDERFLOW */

When strlen(vc_username) >= 76: - Line 170 strncpy writes 76 bytes with no NUL terminator. - index(pc, 0) scans past them and finds the still-zero byte at buf[78] (left by the line-167 bzero), so pc = buf+78. - if (pc < pe-1)buf+78 < buf+79TRUE. - Line 172 *(pc++)='@' clobbers that zero, pc = buf+79. - Line 173 strncpy(pc, vc_srvname, pe-pc-2) → bound = (buf+80)-(buf+79)-2 = -1 → as size_t = SIZE_MAX (18446744073709551615).

strncpy then writes vc_srvname + ~SIZE_MAX zero-padding bytes past f_mntfromname[80] into the adjacent struct statfs fields (f_spares2, f_spare[]), then into struct mount's mnt_vstat, mnt_data, mnt_cred, mnt_vn_*_ops, etc. → mount-time heap overflow.

vc_username can be 76–127 chars: ioc_user is char[SMB_MAXUSERNAMELEN+1] = char[129] (sys/netproto/smb/smb_dev.h:76, SMB_MAXUSERNAMELEN=128 smb.h:289), populated via smb_strdup(vcspec->username) at smb_conn.c:482. Userspace mount_smbfs caps at < SMB_MAXUSERNAMELEN = 127 (contrib/smbfs/lib/smb/ctx.c:288).

Threat model / privilege boundary

This is a mount-time overflow. mount(2) on smbfs requires root (or vfs.usermount=1 + an owner-configured mount). It is therefore a root→kernel hardening gap, not an unprivileged LPE: root→kernel is game-over by definition, so there is no privilege boundary to cross and no uid=0 chain to develop. The realistic impact ceiling is a root-triggered kernel panic / memory corruption (the overflow writes attacker-controlled vc_srvname bytes + unbounded zero-padding into the live struct mount).

Exploit chain

Not applicable — this is a root→kernel write (mount requires root), so there is no unprivileged privilege boundary to cross. Per the audit's threat model, root→kernel corruption is a hardening gap, not an LPE; no uid=0 escalation chain is developed. The primitive characterization:

  • Bucket / object: the victim is the struct mount itself (one kmalloc'd object). The overflow starts at f_mntfromname[80] and runs forward through the rest of struct statfs and into mnt_vstat, mnt_data, mnt_time, mnt_iosize_max, the mnt_vn_*_ops vector, mnt_cred, mnt_refs, etc.
  • Write size / control: vc_srvname bytes (attacker-controlled, up to SMB_MAXSRVNAMELEN-1 = 14 chars) followed by unbounded zero-padding. The first write byte lands at f_mntfromname[79]; zeros begin at f_mntfromname[80].
  • Realistic effect: on the default GENERIC kernel (INVARIANTS ON) the SIZE_MAX write page-faults into unmapped memory → immediate kernel panic. On a no-INVARIANTS kernel the zero-padding silently corrupts the mount's own metadata fields.

Why the live overflow panic is masked (environmental blocker)

smbfs_mount is only reachable after a kernel SMB VC + share are set up via /dev/nsmb (SMBIOC_LOOKUPsmb_sm_lookupsmb_vc_createsmb_vc_connect). On this 6.5-DEVELOPMENT #0 guest, smb_vc_connect calls smb_iod_request(vcp->vc_iod, …) with vc_iod == NULL, panicking at smb_iod_request+0x58 (lock xaddl %edx,0x58(%rbx), fault addr 0x58). This panic fires identically for 9-char and 80-char usernames and fires during SMBIOC_LOOKUP, before mount(2) (and therefore before the line-170 overflow) is ever reached. It is a separate bug in the smb iod setup path, not DF-0907. The overflow is nonetheless confirmed present in the shipping kernel module by disassembly (see below).

Disassembly proof (shipping /boot/kernel/smbfs.ko, smbfs_mount)

Vulnerable bound computation in the live module:

ab33:  lea    0x1b2(%rbx),%r12     # r12 = pe = mp+0x1b2 (end of f_mntfromname)
abde:  sub    %rdi,%rdx            # rdx = pe - pc
abe8:  sub    $0x2,%rdx            # rdx = pe - pc - 2  (-> -1 for 80-char user)
abec:  mov    0x78(%rax),%rsi      # rsi = vcp->vc_username
abf0:  callq  strncpy              # strncpy(pc, username, pe-pc-2)  [bound 76]
...
ad6e:  mov    %r12,%rdx            # rdx = pe
ad71:  sub    %rdi,%rdx            # rdx = pe - pc_new
ad78:  sub    $0x2,%rdx            # rdx = pe - pc_new - 2  -> 0xFFFFFFFFFFFFFFFF
ad7c:  callq  strncpy              # strncpy(pc, srvname, SIZE_MAX)  *** OVERFLOW ***

Relocations in smbfs_mount (original): strncpy ×3, index ×2 — the vulnerable f_mntfromname builder.

Fix validation (Phase 8)

fix.diff replaces the entire strncpy+index+pe-pc-2 block with a single bounded call:

(void)ksnprintf(mp->mnt_stat.f_mntfromname, sizeof(mp->mnt_stat.f_mntfromname),
    "//%s@%s/%s", vcp->vc_username, vcp->vc_srvname, ssp->ss_name);

ksnprintf truncates to sizeof(f_mntfromname) and always NUL-terminates; there is no pointer arithmetic and no size_t coercion, so the underflow is structurally impossible.

Built the fixed smbfs.ko (make -C /usr/src/sys/vfs/smbfs, END_RC=0, no warnings/errors — see fix_build.log).

Validated at the object level by disassembling the fixed module's smbfs_mount: - Original smbfs_mount relocations: strncpy ×3, index ×2, plus the sub $0x2 underflow arithmetic and lea 0x1b2 (pe) — the vulnerable path. - Fixed smbfs_mount relocations: ksnprintf ×1; zero strncpy, zero index, no sub $0x2, no lea 0x1b2. The SIZE_MAX-bound strncpy is gone.

A live before/after kernel test of the overflow itself is not testable on this guest: both the original and fixed modules panic identically at smb_iod_request+0x58 during SMBIOC_LOOKUP (the separate iod-setup bug), before smbfs_mount is reached. The fix is therefore validated at the code/object level (the vulnerable code path is provably eliminated), which is the strongest validation achievable given the environmental blocker.

PoC changes

  • trigger.c — kernel trigger harness: opens /dev/nsmb, issues SMBIOC_LOOKUP with an 80-char ioc_user, then mount(2). Reaches smbfs_mount's line-170 builder (where the overflow lives) on a kernel whose VC setup path is healthy.
  • control.c — identical harness with a 9-char username (< 76, no overflow). Used to prove the smb_iod_request+0x58 panic is the separate iod-setup bug, not the overflow (both control and trigger crash identically).
  • diag_lookup.c — SMBIOC_LOOKUP-only diagnostic that isolates the iod panic to the VC-setup phase.
  • harness.c — userspace deterministic transcription of the pe-pc-2 arithmetic; prints the -1 → SIZE_MAX underflow.
  • fake139.c — minimal TCP listener on 127.0.0.1:139 (used to test whether letting the iod TCP-connect succeed changes the failure path; it does not).
  • fix.diffksnprintf replacement of the vulnerable builder.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable LIVE but VALIDATED at the object/disassembly level. The DF-0907 overflow cannot be triggered live on this guest because a separate smb_iod_request NULL-deref (vc_iod==NULL) panics during SMBIOC_LOOKUP -> smb_vc_connect, before smbfs_mount is ever reached; this is identical on the original and fixed module (confirming it is unrelated to the fix). The fix.diff applies cleanly (git apply --check OK), builds into smbfs.ko (make -C /usr/src/sys/vfs/smbfs, END_RC=0, no warnings), and disassembly of the fixed module's smbfs_mount PROVES the vulnerable path is eliminated: original smbfs_mount calls strncpy x3 + index x2 with the 'sub $0x2,%rdx' underflow arithmetic and 'lea 0x1b2' (pe); fixed smbfs_mount calls ksnprintf x1 and has ZERO strncpy, ZERO index, ZERO underflow arithmetic. The SIZE_MAX-bound write is structurally gone.

BEFORE (original /boot/kernel/smbfs.ko) smbfs_mount relocations: abf1 strncpy, abfb index, ad7d strncpy (SIZE_MAX-bound), ad87 index, adaf strncpy; plus 'abe8: sub $0x2,%rdx' and 'ad78: sub $0x2,%rdx'. AFTER (fixed smbfs.ko) smbfs_mount relocations: a511 ksnprintf ONLY -- zero strncpy, zero index, zero 'sub $0x2', zero 'lea 0x1b2'. Build: END_RC=0 (fix_build.log). Live behavior identical before/after (both panic at smb_iod_request+0x58 -- the separate iod bug) confirming the fix neither regresses nor affects the unrelated iod path.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC (kernel unchanged; only the smbfs.ko KLD was rebuilt+swapped)

Confirmed kernel references

Detail

Exploit chain

none (non-corruption-class privilege analysis): this is a mount-time overflow and mount(2) on smbfs requires root, so it is a root->kernel hardening gap with no unprivileged privilege boundary to cross -- root->kernel is game-over by definition, so no uid=0 escalation chain is developed. Primitive characterization for the record: victim object is the struct mount itself (one kmalloc); the overflow starts at f_mntfromname[80] and runs forward through the rest of struct statfs (f_spares2, f_spare[]) into struct mount's mnt_vstat, mnt_data (->NULL), mnt_cred (->NULL), mnt_vn_*_ops vector; write content is attacker-controlled vc_srvname bytes (up to 14 chars) followed by unbounded zero-padding; on default GENERIC (INVARIANTS ON) the SIZE_MAX write page-faults into unmapped memory -> immediate kernel panic.

Evidence (decisive lines)

harness: 'line 173: pe-pc-2 = (buf+80)-(buf+79)-2 = -1 (ptrdiff_t)' / 'coerced to size_t = 18446744073709551615' / 'CONFIRMED: bound is (size_t)-1 == SIZE_MAX'. Shipping /boot/kernel/smbfs.ko smbfs_mount relocations: 'abf1: R_X86_64_PLT32 strncpy-0x4' / 'abfb: R_X86_64_PLT32 index-0x4' / 'ad7d: R_X86_64_PLT32 strncpy-0x4' (the SIZE_MAX-bound call) / 'ad87: R_X86_64_PLT32 index-0x4' / 'adaf: R_X86_64_PLT32 strncpy-0x4', with 'abe8: sub $0x2,%rdx' and 'ad78: sub $0x2,%rdx' (the underflow arithmetic). Live trigger panic (separate iod bug, masks the overflow): 'Stopped at smb_iod_request+0x58: lock xaddl %edx,0x58(%rbx)' (rbx=0, fault addr 0x58), identical for 9-char and 80-char usernames.

PoC changes

Created findings/poc/DF-0907/ from scratch. trigger.c: kernel harness (open /dev/nsmb + SMBIOC_LOOKUP with 80-char ioc_user + mount(2)) reaching smbfs_mount line 170. control.c: identical harness with 9-char username proving the smb_iod_request panic is a separate iod-setup bug (both crash identically). diag_lookup.c: SMBIOC_LOOKUP-only diagnostic isolating the iod panic to the VC-setup phase. harness.c: userspace deterministic transcription of the pe-pc-2 arithmetic printing the -1 -> SIZE_MAX underflow. fix.diff: ksnprintf replacement of the vulnerable strncpy+index+arithmetic block.

Verified recommended fix

Replace the entire hand-rolled strncpy+index+pe-pc-2 block at sys/vfs/smbfs/smbfs_vfsops.c:165-178 with a single bounded ksnprintf(mp->mnt_stat.f_mntfromname, sizeof(mp->mnt_stat.f_mntfromname), '//%s@%s/%s', vcp->vc_username, vcp->vc_srvname, ssp->ss_name). ksnprintf truncates to sizeof(f_mntfromname) and always NUL-terminates, so the signed underflow to SIZE_MAX is structurally impossible. Matches the finding proposal's 'ksnprintf' recommendation. The full git-apply-able diff lives in findings/poc/DF-0907/fix.diff (validated: git apply --check OK; builds into smbfs.ko END_RC=0).

Verdict

REPRODUCED at the code/object level. The signed-underflow heap overflow in smbfs_mount (sys/vfs/smbfs/smbfs_vfsops.c:170-173) is definitively real and confirmed by three independent lines of evidence. (1) Source analysis: the strncpy bound is computed as pe-pc-2 (ptrdiff_t); when strlen(vc_username)>=76 the second strncpy at :173 evaluates pe-pc-2 = (buf+80)-(buf+79)-2 = -1, which coerces to size_t = SIZE_MAX (18446744073709551615). MNAMELEN=80 (sys/sys/mount.h:93), SMB_MAXUSERNAMELEN=128 so 76-127-char usernames are accepted (smb_dev.h:76, contrib/smbfs/lib/smb/ctx.c:288). (2) Disassembly of the shipping /boot/kernel/smbfs.ko: smbfs_mount contains 'lea 0x1b2(%rbx),%r12' (pe), 'sub $0x2,%rdx' (the underflowing arithmetic at offset 0xabe8 and 0xad78), and a call to strncpy with the SIZE_MAX result at smbfs_mount+0x251. (3) The deterministic arithmetic harness (harness.c) prints 'pe-pc-2 = -1 (ptrdiff_t)', 'coerced to size_t = 18446744073709551615', 'CONFIRMED: bound is (size_t)-1 == SIZE_MAX'. A live kernel panic FROM THE OVERFLOW itself could not be captured because a separate, unrelated bug in the smb VC-setup path (smb_iod_request called with vc_iod==NULL during SMBIOC_LOOKUP -> smb_vc_connect, faulting at smb_iod_request+0x58) panics first, before smbfs_mount is reached; a 9-char control username crashes identically, proving that panic is the iod bug, not DF-0907.