DF-0598 / leak_sample.txt
# DF-0598 โ leak sample (code-level proof harness output)
# Source: ./run.sh 10 and ./run.sh 1000 on the unpatched #0 guest.
# The harness replicates smb_sm_lookupint (sys/netproto/smb/smb_conn.c:123-180)
# line-for-line in userspace. Each failing lookup leaks exactly one VC ref
# onto the LAST VC visited by SMBCO_FOREACH (the stale post-loop `vcp`).
================ run.sh 10 (10 failing lookups) ================
=== 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)
=== FIXED smb_sm_lookupint (fix.diff applied) ===
call #0: rc=1 (nonzero => lookup FAILED), *vcpp=(null) โ correct
after 10 failed lookups:
usecount[alice@a] = 1 (baseline 1, delta +0) <-- 0 leaked refs
usecount[bob@b] = 1 (baseline 1, delta +0)
usecount[carol@c] = 1 (baseline 1, delta +0)
TOTAL leaked refs across all VCs = 0 (expected 0)
================ run.sh 1000 (1000 failing lookups) ================
=== VULNERABLE smb_sm_lookupint ===
after 1000 failed lookups:
usecount[alice@a] = 1001 (baseline 1, delta +1000) <-- linear, unbounded
usecount[bob@b] = 1 (baseline 1, delta +0)
usecount[carol@c] = 1 (baseline 1, delta +0)
TOTAL leaked refs across all VCs = 1000 (expected 1000)
=== FIXED smb_sm_lookupint ===
after 1000 failed lookups:
TOTAL leaked refs across all VCs = 0 (expected 0)
================ INTERPRETATION ================
The leak is deterministic and linear: N failing lookups โ N leaked refs,
all pinned onto ONE VC (the last one SMBCO_FOREACH visited before the list
exhausted). That VC's co_usecount grows without bound and can never return
to 0, so smb_co_gone()'s drain loop (smb_conn.c:263-265 `while (co_usecount > 0)
tsleep(...)`) hangs the closing thread forever. The fix (clearing vcp on
every unlock/continue path + an `error==0` guard on the post-loop ref block)
produces exactly 0 leaked refs at any scale.