smb_strdupin ignores copyin return value β TOCTOU race leaks uninitialized kernel heap memory to SMB server via TRANS2 name
Summary
smb_subr.c:113-131 smb_strdupin first loop reads user string byte-by-byte via copyin to find NUL and compute len. Then kmalloc(len M_WAITOK) NOT zeroed. Then copyin(s,p,len) at :129 β return value IGNORED. TOCTOU: racing thread munmap/mprotect page in string between length loop and bulk copyin. M_WAITOK may sleep widening race window. On bulk copyin fault p is partially filled with stale slab contents (freed cred buffers socket addresses password fragments). Returned as t2p->t_name (smb_usr.c:308). strlen(t2p->t_name) may walk past allocation if NUL byte was on unmapped page. Name transmitted to attacker-controlled SMB server via TRANS2 request (smb_rq.c:635). Impact: kernel heap info leak to network. Requires root (device 0700) + race win. Fix: check copyin return kfree+return NULL on failure add M_ZERO.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0716 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| strdup_test.c | trigger-source | kernel module test harness: creates /dev/strdup_test, calls smb_strdupin directly on ioctl (bypasses SMB protocol) | 3.3 KB | view raw |
| strdup_race.c | trigger-source | userspace race driver: 128-byte string spanning page boundary, racing thread toggles page B PROT_NONE<->PROT_READ | 5.8 KB | view raw |
| Makefile | build-script | bsd.kmod.mk Makefile for strdup_test.ko kernel module | 364 B | β download |
| build.sh | build-script | builds strdup_test.ko + strdup_race | 523 B | view raw |
| run.sh | run-script | kldload smbfs + strdup_test.ko, runs race driver | 713 B | view raw |
| fix.diff | suggested-fix | check copyin return + add M_ZERO in smb_strdupin (smb_subr.c:128-130) | 393 B | view raw |
| run.log | run-log | unpatched run: RACE WON iter 1, stale bytes b03b020000000000 at positions 120-127 | 2.6 KB | view raw |
| fix_build.log | fix-build-log | patched smbfs.ko module build output (full) | 20.0 KB | view raw |
| fix_run.log | fix-run-log | patched race test: RACE WON=0 in 2.5M iterations, no regression | 1.5 KB | view raw |
| fix_disasm.txt | fix-verification | disassembly of fixed smb_strdupin: M_ZERO(0x102), copyin return checked, kfree on failure | 206 B | view raw |
| env.txt | environment | uname, kern.version, cc, device perms, sysctls, INVARIANTS status | 1.2 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, TOCTOU race, race-win evidence, fix validation | 7.8 KB | β raw |
| README.md | readme | summary + build/run/expected + why test harness module | 2.3 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-0716 β smb_strdupin ignores copyin return value β TOCTOU race
Summary
smb_strdupin (sys/netproto/smb/smb_subr.c:113-131) calls copyin(s, p, len)
at line 129 but ignores its return value. A TOCTOU race (racing thread
toggling a page between the length loop and the bulk copyin) causes the bulk
copyin to fault partway, leaving the kmalloc'd buffer (NOT zeroed) partially
filled with stale kernel heap data from a previous M_SMBSTR allocation.
The non-NULL buffer is returned to the caller (smb_usr.c:308) and used as
t2p->t_name β transmitted to the attacker-controlled SMB server via TRANS2.
Build
# Build the kernel module test harness + userspace race driver
make # strdup_test.ko (kernel module)
cc -o strdup_race strdup_race.c -lpthread
Run
# Must be root: load smbfs.ko (provides smb_strdupin) + test harness, then race
kldload smbfs
kldload ./strdup_test.ko
./strdup_race [iterations] # default 100000
Expected (bug present, unpatched)
Most runs: RACE WON: 0 (the race is extremely narrow β ~1 in 200K+).
Occasionally: RACE WON: N with stale bytes (e.g. b0 3b 02 00 00 00 00 00)
at the page-boundary straddling positions (bytes 120-127), proving the copyin
return is ignored and stale slab contents are returned.
Expected (fixed)
RACE WON: 0 always β the fixed smb_strdupin checks the copyin return and
returns NULL on failure (caller handles NULL β ENOMEM). No stale bytes possible.
Why a test harness module?
smb_strdupin is only reachable via SMBIOC_T2RQ β smb_usr_t2request
(smb_usr.c:308), which requires sdp->sd_share != NULL (smb_dev.c:218),
which requires SMBIOC_OPENSHARE β smb_smb_treeconnect β a live SMB server.
No SMB server is available on the audit guest. The strdup_test.ko module calls
smb_strdupin directly (bypassing the SMB protocol) to characterize the
primitive. This is a test harness, not an exploit β the finding is root-only
and there is no escalation.
Fix
fix.diff changes smb_strdupin (smb_subr.c:128-130):
1. Add M_ZERO to kmalloc β defense-in-depth (buffer zeroed even if copyin fails).
2. Check copyin return β if non-zero, kfree(p) and return NULL.
This matches the pattern already used by smb_memdupin (smb_subr.c:144-147).
DF-0716 β smb_strdupin ignores copyin return value β TOCTOU race
Verdict
REPRODUCED β rootβkernel TOCTOU info leak. The code defect is real and
confirmed by source trace (smb_subr.c:129 ignores copyin return) and by a
demonstrated race win (stale slab bytes b0 3b 02 00 00 00 00 00 at positions
120-127 of the returned buffer). The fix (check copyin return β kfree +
return NULL; add M_ZERO) is validated: disassembly of the fixed smbfs.ko
confirms all three changes compiled in, and the race produces zero stale-byte
results on the patched module.
Severity / impact
Low β rootβkernel heap info leak via TOCTOU race. The only path to
smb_strdupin is via /dev/nsmb (the nsmb clone device), created 0700
root:wheel (smb_dev.c:355-356). An unprivileged user cannot open the device
(vfs.usermount=0, no devfs rules loosening nsmb). Additionally, smb_subr.c
is optional netsmb (sys/conf/files:1876) β NOT compiled into the default
X86_64_GENERIC kernel; it only exists in the smbfs.ko loadable module,
which requires root kldload. No memory-corruption primitive is derived β
the leak is stale slab contents (previous M_SMBSTR allocations) sent to an
SMB server that root voluntarily connected to. This is a hardening gap, not
an escalation vector.
Mechanism (trigger β primitive β effect)
The code defect
smb_strdupin (smb_subr.c:113-131):
char *
smb_strdupin(char *s, int maxlen)
{
char *p, bt;
int len = 0;
for (p = s; ;p++) { // length loop: reads byte-by-byte
if (copyin(p, &bt, 1)) // checks copyin return β OK
return NULL;
len++;
if (maxlen && len > maxlen)
return NULL;
if (bt == 0)
break;
}
p = kmalloc(len, M_SMBSTR, M_WAITOK); // NOT zeroed (no M_ZERO)
copyin(s, p, len); // return IGNORED β THE BUG
return p;
}
Line 129: copyin(s, p, len) β the return value is discarded. If copyin
fails (EFAULT), p is partially filled with stale slab contents and returned
as non-NULL. The caller (smb_usr.c:308-312) checks for NULL but the buffer
is non-NULL, so it proceeds to use it as t2p->t_name.
TOCTOU race
- Thread A: issues
SMBIOC_T2RQwithioc_nameβsmb_strdupinstarts. - Length loop reads the user string byte-by-byte (checks copyin return).
Computes
len. kmalloc(len, M_SMBSTR, M_WAITOK)β may sleep (widens race window).- Thread B:
mprotect(page_b, PROT_NONE)β makes a page within the string unreadable. - Bulk
copyin(s, p, len)β faults at thePROT_NONEpage. Returns EFAULT. - Return value IGNORED β
preturned with stale bytes where the faulted page's data should be. pβt2p->t_nameβsmb_t2_request(t2p)β sent to SMB server via TRANS2.
Demonstrated race win
Using a test harness module (strdup_test.ko) that calls smb_strdupin
directly (bypassing the SMB protocol, which requires a live server not
available on the guest), with a 128-byte string spanning a page boundary
(127 bytes on page A + NUL on page B):
[RACE WON iter 1] result bytes (hex, first 128): 4141414141414141414141414141414141414141414141414141414141414141 4141414141414141414141414141414141414141414141414141414141414141 4141414141414141414141414141414141414141414141414141414141414141 414141414141414141414141414141414141414141414141b03b020000000000
- Bytes 0-119:
0x41('A') β correctly copied by bulkcopyin. - Bytes 120-127:
b0 3b 02 00 00 00 00 00β stale slab contents.
The bulk copyin reads in 8-byte chunks. The last chunk (bytes 120-127)
straddled the page boundary (bytes 120-126 on page A, byte 127 on page B).
When the racing thread set page B to PROT_NONE, the 8-byte read faulted,
leaving bytes 120-127 as stale heap data from a previous M_SMBSTR allocation.
The stale bytes (0x0000000000023bb0 in LE) are real kernel heap data
(debug.use_weird_array=0, so no 0xdeadc0de slab poisoning by default) β
potentially containing pointers, credential fragments, or other sensitive data
from previous M_SMBSTR allocations (SMB strings, passwords, addresses).
The race is extremely narrow (~1 win in 200K+ iterations across 2.6M total
iterations). The kmalloc(M_WAITOK) between the length loop and the bulk
copyin is the only widening factor, and it does not reliably sleep on this
guest. The code defect is confirmed regardless by source trace.
Exploit chain
Not applicable β this is a pure info-leak / hardening gap, not a memory-corruption primitive. The stale bytes are read from the slab and sent to the SMB server; there is no write, no UAF, no type confusion. No heap grooming, no victim object, no escalation chain is possible. The finding is correctly classified as Low severity (rootβkernel heap info leak).
Additionally, the path is root-only (device 0700 + kldload), so there is
no privilege boundary to cross. This is a valid hard blocker for escalation
per Phase 6: "reachable only from an already-root context."
PoC changes
Authored from scratch (no prior PoC existed in findings/poc/DF-0716/):
- strdup_test.c β kernel module test harness that creates /dev/strdup_test
(root-only 0600) and calls smb_strdupin directly on ioctl. Bypasses the
SMB protocol (which requires a live server not available on the guest).
- strdup_race.c β userspace driver that mmaps a 2-page buffer, places a
128-byte string spanning the page boundary, and races smb_strdupin by
toggling page B protection (PROT_NONE β PROT_READ|PROT_WRITE) via a
pthread. Examines each result for stale bytes.
- Makefile β builds strdup_test.ko via bsd.kmod.mk.
- build.sh / run.sh β exact build/run commands.
- fix.diff β git-apply-able unified diff: check copyin return + add
M_ZERO in smb_strdupin (smb_subr.c:128-130).
Fix validation (Phase 8)
Baseline (unpatched #0 kernel + unpatched smbfs.ko)
- Race won once in 200K iterations: stale bytes
b0 3b 02 00 00 00 00 00at positions 120-127. - Code defect confirmed by source trace: line 129 ignores
copyinreturn. - Subsequent runs (2.4M+ iterations) did not win again β the race is extremely narrow (~1 in 200K+).
Patched (fixed smbfs.ko module)
- Applied
fix.diffto/usr/src/sys/netproto/smb/smb_subr.c. - Rebuilt
smbfs.komodule (makeinsys/vfs/smbfs/). - Installed to
/boot/kernel/smbfs.ko, loaded. - Disassembly confirms all three fix changes compiled in:
mov $0x102,%edxβM_WAITOK(0x2) | M_ZERO(0x100)= 0x102.test %eax,%eax+jeβcopyinreturn value checked.callq kfree+xor %ebx,%ebxβ on failure,kfree(p)and return NULL.- Race test: 2.5M iterations across 3 runs β
RACE WON: 0(consistent with the fix:copyinfailure now returns NULL, not stale bytes). - No regression: normal case (
copyinsucceeds) returns correct string.
fix_status: fixed
The code path is closed by disassembly verification + the race behavior is consistent (RACE WON = 0 on fixed, RACE WON = 1 on unfixed in the earlier run). The race is too narrow for a statistical before/after comparison on every run, but the disassembly is definitive.
Recommended fix
fix.diff changes smb_strdupin (sys/netproto/smb/smb_subr.c:128-130) from:
p = kmalloc(len, M_SMBSTR, M_WAITOK);
copyin(s, p, len);
return p;
to:
p = kmalloc(len, M_SMBSTR, M_WAITOK | M_ZERO);
if (copyin(s, p, len) != 0) {
kfree(p, M_SMBSTR);
return NULL;
}
return p;
This follows the same pattern already used by smb_memdupin (smb_subr.c:144-147),
which correctly checks the copyin return and frees on failure. Supersedes
the finding proposal (the finding summary mentions "check copyin return
kfree+return NULL on failure add M_ZERO" β the implemented fix matches this
exactly).
Fix verification
fixedVALIDATED: baseline RACE WON=1 in 200K (stale heap bytes); patched RACE WON=0 in 2.5M x3 (M_ZERO + copyin checked + kfree). Disassembly confirms all 3 changes compiled in.
BEFORE: RACE WON=1, stale b03b020000000000. AFTER: RACE WON=0 in 2.5M x3. Disasm: mov $0x102 (M_ZERO) + test %eax (return checked) + callq kfree (free on failure).
Confirmed kernel references
Detail
Exploit chain
none -- pure TOCTOU info-leak / hardening gap. No write, no UAF. Root-only (no privilege boundary to cross).
Evidence (decisive lines)
baseline: RACE WON=1 in 200K iterations, stale bytes b03b020000000000 at positions 120-127. patched: RACE WON=0 in 2.5M iterations x3 runs (M_ZERO + copyin return checked + kfree on failure).
PoC changes
Authored strdup_test.c (kernel module test harness), strdup_race.c (userspace TOCTOU racer), Makefile, build.sh, run.sh, fix.diff (M_ZERO + copyin return check + kfree), VERDICT.md, manifest.json.
Verified recommended fix
Change smb_strdupin:128-130 to: p=kmalloc(len,M_SMBSTR,M_WAITOK|M_ZERO); if(copyin(s,p,len)!=0){kfree(p,M_SMBSTR);return NULL;} return p;. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0716/fix.diff.
Verdict
REPRODUCED. smb_strdupin:129 calls copyin and discards return value. kmalloc not M_ZERO. TOCTOU race (racing thread mprotects page between strlen loop and bulk copyin) leaves buffer partially filled with stale M_SMBSTR slab contents. Race won once in 200K iterations, showing 8 bytes of real kernel heap data (b03b020000000000). Root-only (/dev/nsmb 0700, kldload smbfs).
No comments yet.