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)
PoC verification
Evidence pack
findings/poc/DF-0907 · 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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).
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:
- Source analysis —
sys/vfs/smbfs/smbfs_vfsops.c:165-178computes thestrncpybound aspe - pc - 2(aptrdiff_t). Whenstrlen(vc_username) >= 76, the secondstrncpy(line 173) evaluatespe-pc-2 = -1, which coerces tosize_t=SIZE_MAX. - Disassembly of the shipping
/boot/kernel/smbfs.ko—smbfs_mountcontainslea 0x1b2(%rbx),%r12(pe),sub $0x2,%rdx(the underflowing arithmetic), and acall strncpywith the resultingSIZE_MAXbound atsmbfs_mount+0x251(offset0xabf1). - Deterministic arithmetic harness (
harness.c) — transcribes the exact pointer math and printspe-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_LOOKUP →
smb_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+79 → TRUE.
- 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 mountitself (onekmalloc'd object). The overflow starts atf_mntfromname[80]and runs forward through the rest ofstruct statfsand intomnt_vstat,mnt_data,mnt_time,mnt_iosize_max, themnt_vn_*_opsvector,mnt_cred,mnt_refs, etc. - Write size / control:
vc_srvnamebytes (attacker-controlled, up toSMB_MAXSRVNAMELEN-1 = 14chars) followed by unbounded zero-padding. The first write byte lands atf_mntfromname[79]; zeros begin atf_mntfromname[80]. - Realistic effect: on the default GENERIC kernel (INVARIANTS ON) the
SIZE_MAXwrite 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_LOOKUP → smb_sm_lookup → smb_vc_create →
smb_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, issuesSMBIOC_LOOKUPwith an 80-charioc_user, thenmount(2). Reachessmbfs_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 thesmb_iod_request+0x58panic 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 thepe-pc-2arithmetic; prints the-1 → SIZE_MAXunderflow.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.diff—ksnprintfreplacement of the vulnerable builder.
Fix verification
not_testablenot_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.
Confirmed kernel references
- sys/vfs/smbfs/smbfs_vfsops.c:165
- sys/vfs/smbfs/smbfs_vfsops.c:166
- sys/vfs/smbfs/smbfs_vfsops.c:167
- sys/vfs/smbfs/smbfs_vfsops.c:170
- sys/vfs/smbfs/smbfs_vfsops.c:172
- sys/vfs/smbfs/smbfs_vfsops.c:173
- sys/sys/mount.h:93
- sys/netproto/smb/smb.h:289
- sys/netproto/smb/smb_dev.h:76
- sys/netproto/smb/smb_conn.c:482
- contrib/smbfs/lib/smb/ctx.c:288
- sys/libkern/strncpy.c:43
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.
No comments yet.