smb_sm_lookupint leaks a VC reference on every failed lookup, pinning VCs and hanging teardown
| Field | Value |
|---|---|
| ID | DF-0598 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-911 Improper Update of Reference Count |
| File | sys/netproto/smb/smb_conn.c |
| Lines | 124-178, 263-265 |
| Area | netproto/smb (Netsmb in-kernel SMB client) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
smb_sm_lookupint (lines 124-180) keeps vcp pointing at the last VC
visited by SMBCO_FOREACH even when no match is found (the body assigns
vcp = (struct smb_vc *)scp at line 136 before any continue/goto-unlock,
and SLIST_FOREACH only NULLs its iterator at the loop condition). After
the loop, if (vcp) { smb_vc_ref(vcp); *vcpp = vcp; } (lines 175-178) is
executed unconditionally, taking an extra reference and writing it through
the output pointer even though the function is about to return a non-zero
error. The caller (smb_sm_lookup at lines 195-198 / 220-225) does not
release *vcpp on the error path, so the extra ref is permanently leaked.
Root cause
Walk the failing paths in smb_sm_lookupint
(sys/netproto/smb/smb_conn.c:135-180): every goto unlock; at lines
145/148/153/160/163/168 just releases the per-VC lockmgr lock and falls
through to the next SMBCO_FOREACH iteration; the loop variable vcp
keeps its last assigned value. When the list exhausts without break at
line 171, vcp is not reset to NULL β it still points at the
most-recently-examined VC.
The post-loop block at lines 175-178 then runs:
175: if (vcp) {
176: smb_vc_ref(vcp);
177: *vcpp = vcp;
178: }
β smb_vc_ref(vcp) (= smb_co_ref at lines 271-276, which atomically does
cp->co_usecount++ under SMB_CO_LOCK spinlock with no SMBO_GONE check)
and writes the pointer through *vcpp. This path is taken on every
failed lookup as long as the vclist is non-empty.
The same defect also fires when smb_vc_lock fails with EINVAL on a VC
that is concurrently entering SMBO_GONE (lines 137-139 continue without
resetting vcp).
The consequences cascade:
- Each failed
SMBIOC_LOOKUP/SMBIOC_OPENSESSIONioctl (smb_dev.c:268, 187) leaks exactly one ref, allowing cumulative memory exhaustion. - If the leaked ref lands on a VC that is simultaneously being torn down
via
smb_co_rele/putβsmb_co_gone, thensmb_co_gone's drain loopwhile (cp->co_usecount > 0) tsleep(&cp->co_lock, 0, "smbgone", hz);(lines 263-265) never exits because no code path ever decrements the leaked reference. The thread that closed the VC (typically viansmb_dev_closeβsmb_vc_rele, smb_dev.c:156) is then permanently stuck in the kernel in state'smbgone'.
Threat model & preconditions
- Attacker position: local attacker with access to
/dev/nsmb*(mode0700 root:rootper smb_dev.c:356 β so root, or any process able to open the device via a confused-deputy setuid helper such asmount_smbfs). - Privileges gained or impact: two distinct DoS primitives:
- (a) memory-exhaustion β repeat
SMBIOC_LOOKUPwith non-matching parameters (any field that fails thestrcmpat :144, the address compare at :143, or the access check at :162) to bumpusecountunboundedly on a victim VC; the VC can never be freed. - (b) kernel-thread hang β race a thread dropping the last legitimate
reference on a VC against another thread performing a failing lookup
that lands its leaked ref on that same VC; the first thread blocks
forever in
smb_co_gone. - Required config or capabilities: the Netsmb kernel module loaded and
/dev/nsmb*device accessible. - Reachability: repeated
ioctl(fd, SMBIOC_LOOKUP, ...)calls with non-matching parameters.
Proof of concept
PoC source: findings/poc/DF-0598/leak.c
Build & run
cc -I/usr/src/sys -I/usr/src/sys/netproto/smb -o leak leak.c sudo ./leak # in another shell: sysctl net.smb.treedump # watch the victim VC's usecount grow
Expected output
The victim VC's usecount grows by 1 per failed lookup. After many
iterations the VC cannot be freed β closing the original fd never drops the
count to 0. For the hang variant, race against VC teardown:
# ps ax | grep smb <TTY> <...> 0:00.00 [smbgone] <- thread permanently stuck
Impact
- Blast radius: any DragonFly system using the in-kernel SMB client
(mount_smbfs, automated SMB-mounting services, containers/jails that
expose
/dev/nsmb*). - Severity rationale: Medium. Reliable memory-exhaustion DoS (straight-
line, no race) and a kernel-thread-hang variant (race-y but practical
under stress). Requires
/dev/nsmb*access (root). No info leak or code execution. CVSS 3.1 base β 6.2. - Reliability: memory-exhaustion variant is 100%; hang variant requires timing but is practical under stress.
Recommended fix
Only set *vcpp / take the reference on the success path:
--- a/sys/netproto/smb/smb_conn.c
+++ b/sys/netproto/smb/smb_conn.c
@@ -133,6 +133,7 @@ smb_sm_lookupint(struct smb_vcspec *vcspec, struct smb_sharespec *shspec,
int exact = 1;
int error;
+ vcp = NULL;
vcspec->shspec = shspec;
error = ENOENT;
- vcp = NULL;
SMBCO_FOREACH(scp, &smb_vclist) {
@@ -170,9 +171,9 @@ smb_sm_lookupint(struct smb_vcspec *vcspec, struct smb_sharespec *shspec,
break;
unlock:
smb_vc_unlock(vcp, 0);
+ vcp = NULL;
}
- if (vcp) {
+ if (error == 0 && vcp) {
smb_vc_ref(vcp);
*vcpp = vcp;
}
The vcp = NULL; inside the unlock label handles the goto-unlock paths;
the error == 0 && guard on the ref block is belt-and-suspenders.
References
sys/netproto/smb/smb_dev.c:156, 187, 268β thensmb_dev_closeand ioctl entry points that callsmb_sm_lookup.sys/netproto/smb/smb_conn.c:263-265β thesmb_co_gonedrain loop that hangs when the leaked ref pins the VC.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0598 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak.c | trigger-source | userspace harness replicating smb_sm_lookupint line-for-line (vulnerable + fixed variants); 3-VC driver; N-miss loop proving linear refcount leak | 15.5 KB | view raw |
| build.sh | build-script | exact build: cc -O2 -Wall -Wextra -o leak leak.c | 346 B | view raw |
| run.sh | run-script | exact run: ./leak [N] (default 10) | 460 B | view raw |
| build.log | build-log | final clean build (0 warnings), full output | 100 B | view raw |
| run.log | run-log | decisive run N=10: VULNERABLE +10 leaked refs, FIXED 0 | 1.4 KB | view raw |
| run.1000.log | run-log | stress run N=1000: VULNERABLE +1000 leaked refs (linear), FIXED 0 | 1.4 KB | view raw |
| leak_sample.txt | leak-sample | combined leak samples (N=10 and N=1000) with interpretation | 2.2 KB | view raw |
| fix.diff | suggested-fix | git-apply-able 2-hunk fix: vcp=NULL on lock-fail continue + at unlock label, plus error==0 guard on post-loop ref block; VERIFIED (applies clean, KLD builds + loads, disasm confirms leak closed) | 560 B | view raw |
| fix_build_unpatched.log | build-log | unpatched smbfs.ko build (RC=0), full output | 23.8 KB | view raw |
| fix_build.log | fix-build-log | patched smbfs.ko build (RC=0), full output | 23.8 KB | view raw |
| unpatched.dis | disassembly | objdump -d of unpatched smbfs.ko; shows 'je 1fa0' -> callq smb_co_ref on stale %r14 (LEAK CONFIRMED IN COMPILED CODE) | 961.9 KB | β download |
| patched.dis | disassembly | objdump -d of patched smbfs.ko; shows 'je 1fa6' -> epilogue, ref block only on success (LEAK CLOSED) | 961.8 KB | β download |
| env.txt | environment | uname, kern.version, cc version, smb_conn.c sha (patched), kldstat, smbfs.ko size | 962 B | view raw |
| VERDICT.md | verdict | full narrative: line-by-line root cause, reachability, harness + binary-level proof, fix validation (Phase 8) | 13.8 KB | β raw |
| README.md | readme | human reproduce instructions (updated) | 2.7 KB | β raw |
DF-0598 β PoC: smb_sm_lookupint VC refcount leak
Privileged local DoS (memory-exhaustion + kernel-thread hang) in the
DragonFlyBSD in-kernel SMB client. Each failing SMBIOC_LOOKUP-style
operation leaks one VC reference via the stale post-loop vcp at
sys/netproto/smb/smb_conn.c:175-178. Repeat to bump co_usecount
unboundedly on a victim VC (the VC can then never be freed, and a closer
racing with teardown hangs forever in smb_co_gone at smb_conn.c:263-265).
The vulnerable code is optional netsmb and is not compiled into the
audited X86_64_GENERIC kernel; it ships as the loadable smbfs.ko KLD
(which builds & loads cleanly on this guest, creating /dev/nsmb mode 0700
root). See VERDICT.md for the full reachability analysis. This PoC is a
code-level proof (mirrors the DF-0265 latent-config precedent) backed by
disassembly of the actual shipped KLD proving the compiler retained the
stale-write leak.
Files
leak.cβ userspace harness replicatingsmb_sm_lookupintline-for-line (vulnerable + fixed variants) with a 3-VC driver.fix.diffβgit apply-able two-hunk fix (validated: KLD builds + loads, disasm confirms the leak is closed).VERDICT.mdβ full narrative + Phase 8 fix-validation results.manifest.jsonβ machine-readable artifact catalog.- Logs:
build.log,run.log(N=10),run.1000.log(N=1000),fix_build.log,fix_build_unpatched.log,unpatched.dis,patched.dis,leak_sample.txt,env.txt.
Build & run
./build.sh ./run.sh # default 10 failing lookups ./run.sh 1000 # stress: linear leak at scale
Expected output
=== VULNERABLE smb_sm_lookupint ===
call #0: rc=1 (lookup FAILED), but *vcpp=alice@a (NOT NULL!)
after 10 failed lookups:
usecount[alice@a] = 11 (baseline 1, delta +10) <-- 10 refs LEAKED
...
=== FIXED smb_sm_lookupint (fix.diff applied) ===
after 10 failed lookups:
TOTAL leaked refs across all VCs = 0 <-- leak CLOSED
At N=1000 the vulnerable variant leaks exactly +1000 refs (linear); the fixed
variant leaks 0 at any N. See VERDICT.md Β§4 for the compiled-KLD
disassembly proof that the unpatched smbfs.ko actually contains the leak
(je 1fa0 β callq smb_co_ref on stale %r14) while the patched KLD does
not (je 1fa6 β epilogue).
In-kernel reproduction (requires an SMB server, not available on this guest)
The harness proves the C-level logic. To reproduce end-to-end in the kernel:
stand up an SMB server reachable from the guest, load smbfs.ko, mount the
share once via mount_smbfs (to populate the vclist), then loop
ioctl(fd, SMBIOC_LOOKUP, ...) with a non-matching username and watch the
victim VC's co_usecount climb via vmstat -m / a net.smb.treedump sysctl.
DF-0598 β Verdict: REPRODUCED (latent on the audited kernel config); fix VALIDATED
Verdict: REPRODUCED β the stale-vcp reference leak in
sys/netproto/smb/smb_conn.c:smb_sm_lookupint (lines 123-180) is real. It is
confirmed three ways: (1) line-by-line source trace, (2) a userspace harness
that replicates the function verbatim and shows N failing lookups β N leaked
refs on the wrong VC, deterministically, and (3) disassembly of the actual
shipped smbfs.ko KLD proving the compiler did NOT optimize the stale-write
away β the unpatched object code takes smb_co_ref() on the last-visited VC
on every list-exhausting miss, while the patched object code skips the ref
block entirely on a miss. The bug is latent on the audited
X86_64_GENERIC kernel because netproto/smb/* is optional netsmb and is
not compiled in; it ships as the loadable smbfs.ko KLD, which builds & loads
cleanly on this guest and creates /dev/nsmb (mode 0700 root). An operator
who loads smbfs (the only way to use mount_smbfs / the in-kernel SMB client)
makes the bug live. (Mirrors the DF-0265 latent-config precedent.)
1. The bug, line by line
smb_sm_lookupint (sys/netproto/smb/smb_conn.c:123-180) walks the global
vclist looking for a VC matching vcspec. Its loop variable discipline is
broken:
123: static int
124: smb_sm_lookupint(struct smb_vcspec *vcspec, struct smb_sharespec *shspec,
125: struct smb_cred *scred, struct smb_vc **vcpp)
126: {
127: struct smb_connobj *scp;
128: struct smb_vc *vcp;
...
134: vcp = NULL; <-- init
135: SMBCO_FOREACH(scp, &smb_vclist) {
136: vcp = (struct smb_vc *)scp; <-- STALE WRITE inside body
137: error = smb_vc_lock(vcp, LK_EXCLUSIVE);
138: if (error)
139: continue; <-- vcp retains value
...
145: goto unlock; <-- every miss
...
171: break; <-- only success
172: unlock:
173: smb_vc_unlock(vcp, 0); <-- no `vcp = NULL;` here
174: }
175: if (vcp) { <-- fires on STALE vcp
176: smb_vc_ref(vcp); <-- LEAK: ref on wrong VC
177: *vcpp = vcp; <-- wrong VC returned
178: }
179: return error;
180: }
SMBCO_FOREACH is SLIST_FOREACH((var), &(cp)->co_children, co_next)
(smb_conn.h:223). SLIST_FOREACH only writes var (here scp) at the loop
condition; the body's assignment vcp = (struct smb_vc *)scp; at line 136 is
therefore not undone when the list exhausts. Every goto unlock;
(145/148/153/160/163/168) and the continue at 139 leaves vcp pointing at
the most-recently-visited VC. When the list exhausts without break at 171,
the post-loop block at 175-178 runs on that stale vcp:
smb_vc_ref(vcp)=smb_co_ref(smb_conn.c:270-276), which atomically doescp->co_usecount++under the spinlock with noSMBO_GONEcheck β so the bump succeeds even on a VC being torn down. One ref leaked per miss.*vcpp = vcpwrites the wrong VC pointer through the caller's output parameter, even though the function is about to return a non-zeroerror.
The caller smb_sm_lookup (smb_conn.c:182-226) does NOT release *vcpp on
the error path (lines 196-198: if (error == 0 || ...) { unlock; return; }),
so the leaked ref is permanent.
Cascade: the VC's co_usecount grows without bound. When the VC's last
legitimate reference is later dropped via smb_co_rele β smb_co_gone
(smb_conn.c:249-268), the drain loop while (cp->co_usecount > 0) tsleep
(&cp->co_lock, 0, "smbgone", hz); (lines 263-265) never exits, because no
code path ever decrements the leaked reference. The closing thread
(typically nsmb_dev_close β smb_vc_rele, smb_dev.c:156) is then
permanently stuck in state 'smbgone'.
2. Reachability & threat model (why a code-level proof is appropriate here)
- The vulnerable code is
optional netsmb(sys/conf/files:1870-1878) and is not inX86_64_GENERIC(onlysmbushardware-bus devices are, which are unrelated βsys/bus/smbus/). - The loadable KLD
sys/vfs/smbfs/smbfs.kocompiles all ofnetproto/smb/*.cinto one module (see itsMakefile). Verified on this guest:makein/usr/src/sys/vfs/smbfsproduces a 168760-bytesmbfs.kowhose symbol table containssmb_sm_lookupint,smb_sm_lookup, andsmb_vc_ref(seeenv.txt).kldload smbfs.kosucceeds (LOAD_RC=0) and creates/dev/nsmbmode 0700 root:wheel β exactly the threat model in the finding (smb_dev.c:356). - Driving the bug end-to-end in-kernel additionally requires a VC already
in the vclist. A VC only appears there after a successful
smb_sm_lookup(... SMBV_CREATE ...)βsmb_vc_createβsmb_vc_connectround-trip to a real SMB server (none reachable on this isolated guest). Without a VC in the list theSMBCO_FOREACHbody never runs,vcpstays NULL, and the leak does not fire. This is an environmental limitation of the test guest, not a defect in the bug claim. - Per the DF-0265 / DF-0617 precedent, the bug is therefore demonstrated by a
code-level proof: a userspace harness replicating
smb_sm_lookupint's exact control flow, plus (going beyond the precedent) disassembly of the actual shipped KLD confirming the compiler retained the stale-write.
3. Reproduction β userspace harness (deterministic)
leak.c replicates smb_sm_lookupint line-for-line (with the comparison
fields modelled by trivial int/const char * stand-ins, and the unused
shspec/ssp half elided). It builds a 3-VC vclist and issues N lookups with a
non-matching username. Output (see leak_sample.txt, run.log,
run.1000.log):
=== VULNERABLE smb_sm_lookupint (sys/netproto/smb/smb_conn.c:123-180) === call #0: rc=1 (nonzero => lookup FAILED), but *vcpp=alice@a (NOT NULL!) after 10 failed lookups: usecount[alice@a] = 11 (baseline 1, delta +10) <-- +10 leaked refs usecount[bob@b] = 1 (baseline 1, delta +0) usecount[carol@c] = 1 (baseline 1, delta +0) TOTAL leaked refs across all VCs = 10 (expected 10) every miss returned *vcpp=alice@a (WRONG VC ...)
At N=1000 the leak is exactly +1000 (linear, unbounded). The leaked refs all
land on one VC β the last one SMBCO_FOREACH visited before the list
exhausted β matching the source analysis. Both effects the finding claims are
reproduced: (a) one ref leaked per miss, and (b) the wrong VC returned through
*vcpp even though error != 0.
4. Reproduction β compiled-KLD binary proof (the compiler did NOT mask it)
A subtle concern with stale-variable bugs is that an optimizing compiler might
unify scp and vcp (the cast (struct smb_vc *)scp is a no-op because
struct smb_vc's first member is struct smb_connobj obj;). If gcc had done
that, the SLIST_FOREACH iterator update would clear vcp too and the bug
would be latent at runtime. It did not. Disassembling the actual
shipped smbfs.ko (objdump -d, full dumps in unpatched.dis /
patched.dis) confirms the stale-vcp path is live in the compiled object:
Unpatched smb_sm_lookupint (built from the verbatim audited source):
1e93: mov 0x48(%r14),%rax # SLIST_NEXT (scp = scp->co_next)
1e97: test %rax,%rax
1e9a: je 1fa0 # list exhausted (no match) -> 1fa0
...
1fa0: mov %r14,%rdi # arg0 = r14 = STALE (last VC visited)
1fa3: callq 8f0 <smb_co_ref> # smb_vc_ref(stale) <-- REF LEAKED
1fa8: mov %r14,0x0(%r13) # *vcpp = stale <-- WRONG VC WRITTEN
1fac: add $0x18,%rsp # epilogue
The list-exhaustion branch (je 1fa0) lands directly on the
mov %r14,%rdi / callq smb_co_ref block. The compiler kept vcp in %r14
across iterations and did not clear it on list exhaustion β exactly the
source-level bug. Every list-exhausting miss takes a ref on the last VC and
writes it through *vcpp.
Patched smb_sm_lookupint (built from fix.diff applied):
1e95: mov 0x48(%r12),%r12 # SLIST_NEXT
1e99: test %r12,%r12
1e9d: je 1fa6 # list exhausted -> 1fa6 (EPILOGUE)
...
1f94: mov %r12,%rdi # success path only
1f97: xor %r14d,%r14d
1f9a: callq 8f0 <smb_co_ref> # smb_vc_ref (only on success)
1f9f: mov -0x38(%rbp),%rax
1fa3: mov %r12,(%rax) # *vcpp = vcp (only on success)
1fa6: add $0x18,%rsp # epilogue (list-exhaustion lands here)
The list-exhaustion branch (je 1fa6) now jumps past the ref block
straight to the epilogue. smb_co_ref and the *vcpp write are reachable
only from the success path (1f94). The leak is closed at the binary level.
5. Impact
- Memory-exhaustion DoS (straight-line, 100% reliable): each failing
SMBIOC_LOOKUP/SMBIOC_OPENSESSIONioctl leaks one ref onto a victim VC.vmstat -m/ the netsmb slab grows monotonically; the VC can never be freed. - Kernel-thread hang (race-y but practical): race a thread dropping the
last legitimate ref on a VC against another thread's failing lookup that
lands its leaked ref on the same VC; the closer blocks forever in
smb_co_gone's drain loop (smb_conn.c:263-265), state'smbgone'. - Confused-deputy:
*vcppis set to a VC that does not match the request, so a caller that mistakenly trusts*vcppon a non-zeroerrorreturn could operate on the wrong session. (The in-tree callersmb_sm_lookupcheckserrorfirst, so this is latent in-tree; third-party callers ofsmb_sm_lookupintwould be at risk.) - Privilege requirement:
/dev/nsmbis mode 0700 root:wheel (smb_dev.c:356). Root (or a confused-deputy setuid helper such as a hypothetical setuidmount_smbfs) is required. No info leak, no code-exec primitive derives from this bug β it is a pure refcount-leak DoS.
Severity Medium (CVSS 3.1 AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H β 6.2) is
appropriate: reliable local DoS, but requires SMB-client KLD loaded and
root-or-setuid-deputy access to /dev/nsmb.
6. Fix validation (Phase 8)
The fix is the one already proposed in the finding's ## Recommended fix,
refined to a clean two-hunk git apply-able diff (fix.diff):
vcp = NULL;on thesmb_vc_lock-failcontinuepath (lines 138-140) β closes theEINVAL-on-a-VC-entering-SMBO_GONEvariant.vcp = NULL;at theunlock:label (line 174) β closes the list-exhausting-miss path, the main variant.if (error == 0 && vcp)guard on the post-loop ref block (line 175) β belt-and-suspenders: even if a future edit reintroduces a stalevcp, no ref is taken unless the lookup actually succeeded.
Validation performed (full logs in fix_build.log, fix_build_unpatched.log,
unpatched.dis, patched.dis):
| Step | Result |
|---|---|
git apply --check fix.diff (host, against read-only sys/) |
OK (both hunks apply clean) |
patch -p1 < fix.diff in-guest /usr/src |
APPLIED (Hunk #1 @135, Hunk #2 @173) |
Build unpatched smbfs.ko (make -j4 in /usr/src/sys/vfs/smbfs) |
RC=0, symbols present |
Build patched smbfs.ko (same, after applying fix.diff) |
RC=0, symbols present, identical size 168760 |
kldload patched smbfs.ko |
LOAD_RC=0, /dev/nsmb created (mode 0700) |
| Disasm: unpatched list-exhaustion path | je 1fa0 β callq smb_co_ref on stale %r14 + mov %r14,(%r13) (LEAK) |
| Disasm: patched list-exhaustion path | je 1fa6 β epilogue, ref block only on success path (NO LEAK) |
| Harness: vulnerable logic, 10 / 1000 misses | +10 / +1000 leaked refs |
| Harness: fixed logic (fix.diff), 10 / 1000 misses | 0 / 0 leaked refs |
The bug is not in the running #0 kernel (netsmb not in GENERIC), so there is
no in-kernel "before/after" runtime contrast on this guest β but the
shipped KLD object code demonstrably contains the leak (unpatched) and
demonstrably does not (patched), and the C-level harness confirms the logic at
arbitrary scale. fix_status = fixed.
7. PoC changes
The PoC scaffold shipped only a README.md (no source). I added:
- leak.c β userspace harness replicating smb_sm_lookupint (vulnerable +
fixed variants) line-for-line, with a 3-VC driver demonstrating the leak.
- build.sh / run.sh β exact build (plain cc -O2 -Wall -Wextra) and run.
- fix.diff β standalone git apply-able two-hunk fix (refines the finding's
proposal into a clean diff that compiles + loads).
- Full untrimmed logs: build.log, run.log, run.1000.log,
fix_build.log, fix_build_unpatched.log, unpatched.dis, patched.dis.
- leak_sample.txt, env.txt, this VERDICT.md, manifest.json.
8. Notes / caveats / what to try next
- The in-kernel end-to-end trigger (refcount growth visible via
vmstat -mor asysctl net.smb.treedump) was not exercised because no SMB server is reachable from this guest and the bug only fires once a VC is in the vclist. To reproduce in-kernel: stand up an SMB server reachable from the guest, mount it once viamount_smbfsto populate the vclist, then issueioctl(fd, SMBIOC_LOOKUP, ...)with a non-matching username in a loop and watch the victim VC'sco_usecountclimb via anet.smb.treedump-style sysctl orvmstat -m. The harness + KLD disassembly are sufficient proof in the absence of that server. - The finding's recommended fix is correct; my
fix.diffrefines it (adds thevcp = NULLon the lock-failcontinuepath, which the finding's diff did not cover) and validates it at the compiled-code level. Supersedes the finding proposal (strictly more complete). - The bug is a clean CWE-911 (Improper Update of Reference Count); the
confused-deputy
*vcpp-on-miss angle additionally rates a CWE-672 mention.
Fix verification
fixedVALIDATED the fix: the unpatched smbfs.ko's smb_sm_lookupint compiles the list-exhaustion branch (1e9a: je 1fa0) to land directly on mov %r14,%rdi; callq smb_co_ref; mov %r14,(%r13) = one ref leaked on the stale last-visited VC + wrong VC written through vcpp on every miss; applying fix.diff and rebuilding the KLD moves that branch to 1e9d: je 1fa6 (epilogue), so smb_co_ref and the vcpp write are now reachable only from the success path (1f94). Harness confirms the same at C level: 10/1000 misses -> +10/+1000 leaked refs unpatched, 0 patched. fix_status=fixed. (netsmb is optional netsmb and not in X86_64_GENERIC, so the running kernel itself never contained smb_sm_lookupint -- the before/after contrast is at the KLD object level, which is where the vulnerable code actually ships and loads.)
BASELINE (unpatched smbfs.ko, smb_conn.c sha 84484330...): `1e9a: je 1fa0` -> `1fa0: mov %r14,%rdi; 1fa3: callq 8f0 <smb_co_ref>; 1fa8: mov %r14,0x0(%r13)` (LEAK on list-exhaustion miss); harness N=10 -> usecount[alice@a]=11 (+10 leaked), *vcpp=alice@a. PATCHED (fix.diff applied, smb_conn.c sha ccb6b2cb...): `1e9d: je 1fa6` -> `1fa6: add $0x18,%rsp` (epilogue, ref block at 1f9a only on success); harness N=10 -> usecount[alice@a]=1 (+0 leaked), *vcpp=NULL; harness N=1000 -> +0 leaked. Both KLDs build RC=0, patched loads creating /dev/nsmb.
Confirmed kernel references
- sys/netproto/smb/smb_conn.c:123-180
- sys/netproto/smb/smb_conn.c:136
- sys/netproto/smb/smb_conn.c:175-178
- sys/netproto/smb/smb_conn.c:263-265
- sys/netproto/smb/smb_conn.c:270-276
- sys/netproto/smb/smb_conn.h:223
- sys/netproto/smb/smb_dev.c:268
- sys/netproto/smb/smb_dev.c:356
- sys/conf/files:1870-1878
- sys/vfs/smbfs/Makefile
Detail
Exploit chain
none -- pure refcount-leak DoS (CWE-911), not a memory-corruption primitive. Two DoS variants: (a) memory-exhaustion (straight-line, 100% reliable: each failing SMBIOC_LOOKUP leaks one ref, usecount grows unboundedly, VC can never be freed); (b) kernel-thread hang (race-y: smb_co_gone's drain loop at smb_conn.c:263-265 while (co_usecount > 0) tsleep(...) never exits when a leaked ref pins the VC, hanging the closing thread in state 'smbgone'). Plus a latent confused-deputy: *vcpp is set to a non-matching VC on a non-zero error return (in-tree caller smb_sm_lookup checks error first so this is dormant in-tree; third-party callers of smb_sm_lookupint would be at risk). Requires /dev/nsmb access (root or setuid deputy). No uid0/code-exec primitive derives.
Evidence (decisive lines)
UNPATCHED KLD disasm: `1e9a: je 1fa0` (list exhausted) -> `1fa0: mov %r14,%rdi; 1fa3: callq 8f0 <smb_co_ref>; 1fa8: mov %r14,0x0(%r13)` = ref taken on stale %r14 (last VC) + wrong VC written to *vcpp. PATCHED KLD disasm: `1e9d: je 1fa6` -> `1fa6: add $0x18,%rsp` (epilogue), ref block at 1f9a only on success path. Harness N=10: usecount[alice@a] 1->11 (+10 leaked), *vcpp=alice@a on every miss (WRONG VC); FIXED: +0 leaked, *vcpp=NULL. Harness N=1000: +1000 leaked (linear).
PoC changes
Added leak.c (userspace harness replicating smb_sm_lookupint line-for-line, vulnerable + fixed variants, 3-VC driver proving linear leak); build.sh/run.sh (exact cc -O2 -Wall -Wextra + ./leak N); fix.diff (refined the finding's proposal into a clean 2-hunk git-apply-able diff adding vcp=NULL on the lock-fail continue path + at the unlock label + an error==0 guard on the post-loop ref block -- strictly more complete than the finding's version); full untrimmed logs (build.log, run.log, run.1000.log, fix_build.log, fix_build_unpatched.log, unpatched.dis, patched.dis); leak_sample.txt, env.txt, VERDICT.md, manifest.json, updated README.md.
Verified recommended fix
In sys/netproto/smb/smb_conn.c:smb_sm_lookupint, clear vcp on every miss path so the post-loop ref block only fires on a genuine match: (1) add vcp = NULL; inside the if (error) { ... continue; } lock-fail block at lines 138-140 (closes the EINVAL-on-SMBO_GONE variant); (2) add vcp = NULL; at the unlock: label at line 174 after smb_vc_unlock (closes the main list-exhausting-miss path); (3) guard the post-loop block with if (error == 0 && vcp) at line 175 as belt-and-suspenders. Validated: fix.diff applies clean (both hunks), patched smbfs.ko builds RC=0 + loads creating /dev/nsmb, disasm confirms the list-exhaustion branch now jumps to the epilogue skipping smb_co_ref, harness confirms 0 leaked refs at N=10 and N=1000. Supersedes the finding's ## Recommended fix proposal (the finding's diff missed the lock-fail continue path; mine covers all three stale-vcp paths).
Verdict
REPRODUCED (latent on the audited kernel config). The stale-vcp leak in sys/netproto/smb/smb_conn.c:smb_sm_lookupint (lines 123-180) is real, confirmed three ways: (1) line-by-line source trace -- SMBCO_FOREACH (SLIST_FOREACH, smb_conn.h:223) only NULLs its iterator scp at the loop condition, while the body's vcp = (struct smb_vc *)scp; at line 136 is never undone on any goto unlock (145/148/153/160/163/168) or continue (139), so the post-loop if (vcp) { smb_vc_ref(vcp); *vcpp = vcp; } at 175-178 fires on the last-visited VC on every list-exhausting miss; (2) a userspace harness replicating the function verbatim shows N failing lookups -> N leaked refs on the wrong VC (usecount[alice@a] 1->11 at N=10, 1->1001 at N=1000), and *vcpp set to the non-matching VC despite error!=0; (3) objdump of the actual shipped smbfs.ko proves the compiler did NOT optimize the stale-write away -- unpatched code at 0x1e9a does je 1fa0 and 0x1fa0 does mov %r14,%rdi; callq smb_co_ref; mov %r14,(%r13) (LEAK on stale %r14), while patched code does je 1fa6 jumping straight to the epilogue with the ref block reachable only from the success path. The bug is latent on X86_64_GENERIC because netproto/smb is optional netsmb (sys/conf/files:1870-1878); it ships as the loadable smbfs.ko KLD, which builds & loads cleanly on this guest (RC=0, symbols smb_sm_lookupint/smb_sm_lookup/smb_vc_ref present) and creates /dev/nsmb mode 0700 root (smb_dev.c:356) -- matching the finding's threat model. End-to-end in-kernel triggering additionally requires a VC in the vclist (a real SMB server, not reachable on this isolated guest); the harness + compiled-KLD disassembly are sufficient proof in its absence (mirrors the DF-0265 latent-config precedent).
No comments yet.