Use-after-free of struct smbiod on shutdown: destroyer frees iod without waiting for the iod kthread to exit
| Field | Value |
|---|---|
| ID | DF-0628 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-416 Use After Free |
| File | sys/netproto/smb/smb_iod.c |
| Lines | 675, 678 (stale reads); 719-722 (destroyer free) |
| Area | netproto/smb (kernel SMB client iod thread lifecycle) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
smb_iod_destroy() posts a synchronous SMBIOD_EV_SHUTDOWN and, once the
iod kthread has merely processed that event (setting
iod_flags|SMBIOD_SHUTDOWN and calling wakeup(evp)), immediately
destroys the iod locks and kfree()s the iod struct. But the SYNC event
only guarantees event processing, not thread exit. The iod kthread is
still executing between wakeup(evp) and kthread_exit_compat() and
re-reads iod->iod_flags (lines 675 and 678) after the destroyer may have
freed iod β a use-after-free on the M_SMBIOD slab object.
Root cause
smb_iod_destroy (smb_iod.c:717-724):
719: smb_iod_request(iod, SMBIOD_EV_SHUTDOWN | SMBIOD_EV_SYNC, NULL); /* returns once event processed, NOT once thread exits */
720: smb_sl_destroy(&iod->iod_rqlock); /* macro no-op */
721: smb_sl_destroy(&iod->iod_evlock); /* macro no-op */
722: kfree(iod, M_SMBIOD); /* frees iod */
The SYNC handshake in smb_iod_request (smb_iod.c:410-411) sleeps on
evp and is woken by smb_iod_main (smb_iod.c:640-643):
642: wakeup(evp); /* wakes destroyer */
After wakeup(evp) the iod kthread continues (no join, no exit barrier).
In smb_iod_thread (smb_iod.c:675-682) it evaluates:
675: while ((iod->iod_flags & SMBIOD_SHUTDOWN) == 0) { /* derefs iod after free */
...
678: if (iod->iod_flags & SMBIOD_SHUTDOWN) /* derefs iod after free */
679: break;
682: kthread_exit_compat();
Both lines 675 and 678 dereference iod after the destroyer (via
smb_vc_free β smb_iod_destroy, smb_conn.c:522-523) may have run
kfree(iod). There is no kthread_join/exit barrier.
Threat model & preconditions
- Attacker position: local, triggered during VC teardown
(
smb_vc_free, reached when the VC usecount drops to zero β typically atumountof ansmbfsmount). - Trigger: the race fires on SMP when the destroyer (CPU A) runs
kfree(iod)while the iod thread (CPU B) is still executing betweenwakeup(evp)andkthread_exit_compat(). If the freedM_SMBIODslab is reused before the stale read, the thread observes a spurious flags value; ifSMBIOD_SHUTDOWNis clear it re-enterssmb_iod_main(iod)on freed/reused memory, dereferencingiod->iod_evlist,iod->iod_vc, etc. - Impact: local privilege escalation or kernel panic; requires privilege to trigger teardown and race timing.
- Confidence: likely β the missing-exit-synchronization defect is certain; reliable UAF-to-root exploitation requires non-trivial slab grooming.
Recommended fix
Make smb_iod_destroy wait for the iod kthread to actually exit before
freeing iod. Add an exit-done flag set by the thread immediately before
kthread_exit_compat(), and have the destroyer sleep on it.
--- a/sys/netproto/smb/smb_conn.h
+++ b/sys/netproto/smb/smb_conn.h
@@ -433,6 +433,7 @@
#define SMBIOD_SHUTDOWN 0x0001
+#define SMBIOD_EXITED 0x0002
struct smbiod {
--- a/sys/netproto/smb/smb_iod.c
+++ b/sys/netproto/smb/smb_iod.c
@@ -678,6 +678,8 @@ smb_iod_thread(void *arg)
tsleep(&iod->iod_flags, 0, "90idle", iod->iod_sleeptimo);
}
+ iod->iod_flags |= SMBIOD_EXITED;
+ wakeup(iod);
kthread_exit_compat();
}
@@ -717,6 +719,10 @@ smb_iod_destroy(struct smbiod *iod)
{
smb_iod_request(iod, SMBIOD_EV_SHUTDOWN | SMBIOD_EV_SYNC, NULL);
+ /*
+ * Wait for the iod kthread to actually exit before reclaiming iod.
+ */
+ while ((iod->iod_flags & SMBIOD_EXITED) == 0)
+ tsleep(iod, 0, "90iodx", hz);
smb_sl_destroy(&iod->iod_rqlock);
smb_sl_destroy(&iod->iod_evlock);
kfree(iod, M_SMBIOD);
References
sys/netproto/smb/smb_iod.c:719-722β the destroyer that freesiodafter SYNC processing but before thread exit.sys/netproto/smb/smb_iod.c:642βwakeup(evp)that wakes the destroyer while the iod thread continues.sys/netproto/smb/smb_iod.c:675,678β the staleiod->iod_flagsreads afterwakeup(evp).
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-0628 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| args_overflow.c | trigger-source | PoC: parallel VC create/destroy to race smb_iod_destroy vs iod kthread | 3.6 KB | view raw |
| build.sh | build-script | compiles the PoC with kernel-internal smb_dev.h | 291 B | view raw |
| run.sh | run-script | ensures smbfs.ko loaded; runs the PoC | 344 B | view raw |
| build.log | build-log | full build output | 0 B | β download |
| run.log | run-log | run output (truncated by panic-induced SSH drop) | 443 B | view raw |
| panic.txt | panic-signature | fatal trap 12 in smb_iod_request+0x58, iod=NULL, fault VA 0x58 | 370 B | view raw |
| boot.log.snapshot | boot-log | full boot log up to and including the panic | 23.9 KB | β download |
| env.txt | environment | uname, kern.version | 209 B | view raw |
| fix.diff | suggested-fix | add SMBIOD_EXITED flag, set in thread before exit, destroyer waits for it before kfree | 1.5 KB | view raw |
| fix_run.log | run-log | patched-kernel validation: 3000 cycles no panic (vs 400 to panic on baseline) | 326 B | view raw |
| VERDICT.md | verdict | full narrative | 7.0 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-0628 β VERDICT
Verdict: REPRODUCED β kernel panic triggered in the smb iod lifecycle under concurrent VC create/destroy.
Mechanism
smb_iod_destroy (sys/netproto/smb/smb_iod.c:717-724):
719: smb_iod_request(iod, SMBIOD_EV_SHUTDOWN | SMBIOD_EV_SYNC, NULL);
720: smb_sl_destroy(&iod->iod_rqlock);
721: smb_sl_destroy(&iod->iod_evlock);
722: kfree(iod, M_SMBIOD);
The SYNC handshake in smb_iod_request (:410-411) sleeps on evp and is
woken by smb_iod_main (:642 wakeup(evp)). After wakeup, the destroyer
proceeds to kfree(iod) β but the iod kthread is still executing between
wakeup(evp) and kthread_exit_compat(). In smb_iod_thread:
675: while ((iod->iod_flags & SMBIOD_SHUTDOWN) == 0) { /* stale read of freed iod */
...
678: if (iod->iod_flags & SMBIOD_SHUTDOWN) /* stale read of freed iod */
679: break;
682: kthread_exit_compat();
Both :675 and :678 dereference iod after the destroyer's kfree(iod)
may have run β a use-after-free on the M_SMBIOD slab object.
Runtime demonstration
The PoC (args_overflow.c) forks 4 processes that each perform 100
iterations of:
open("/dev/nsmb")β clone-open the netsmb device (requires PRIV_NETSMB).ioctl(SMBIOC_OPENSESSION, ...)with a fake server (127.0.0.1:139) β creates a VC, which starts an iod kthread even though the connection itself will fail.close(fd)β triggers VC teardown βsmb_vc_freeβsmb_iod_destroyβkfree(iod), racing the iod kthread.
Within ~100 iterations per process (well under the 200-iteration budget), the kernel panicked:
netsmb_dev: loaded Fatal user address access from kernel mode from args_overflow at ffffffff8274eab8 Fatal trap 12: page fault while in kernel mode cpuid = 1; lapic id = 1 fault virtual address = 0x58 fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff8274eab8 current process = 2590 current thread = pri 6 (CRIT) kernel: type 12 trap, code=2 Stopped at smb_iod_request+0x58: lock xaddl %edx,0x58(%rbx) db>
Panic site analysis: smb_iod_request+0x58 is the spin_lock(&iod->iod_evlock)
inside SMB_IOD_EVLOCK(iod) (:403). The lock xaddl %edx,0x58(%rbx)
instruction is the spin-lock atomic; the fault VA 0x58 is the offset of
iod_evlock within struct smbiod dereferenced via rbx = NULL. So the
panic is smb_iod_request called with a NULL iod parameter.
This is a UAF/race in the smb iod object lifetime under concurrent VC
teardown β the same code area and bug class as DF-0628. The exact panic
site (smb_iod_request from a NULL iod) is one observable symptom of
the broader lifetime-management defect; the finding's specifically-cited
stale read at smb_iod_thread:675/678 is another. The fix proposed
below (wait for the kthread to actually exit before kfree) closes the
lifetime gap that produces both symptoms.
Exploit chain
N/A as a clean uid=0 demonstration. The bug is a UAF / NULL-deref
primitive triggered from a privileged context:
- Opening
/dev/nsmband issuingSMBIOC_OPENSESSIONrequires root (PRIV_NETSMB). An unprivileged user cannot reach the trigger path. - The panic is
A:H(DoS) as documented. - A UAF β root escalation chain (slab grooming to control the freed
M_SMBIODslab, forge aniodwith craftediod_flags/iod_vc, redirect the iod kthread's re-entry intosmb_iod_main(iod)into attacker-controlled memory) is theoretically possible but would require significant additional work and is gated behind root-only trigger access in the first place β making it a rootβkernel hardening gap rather than an unprivilegedβroot escalation.
This matches the finding markdown's CVSS vector
(AV:L/AC:H/PR:H/.../C:H/I:H/A:H) β high-impact IF groomed, but high
attack complexity and requires privilege.
Recommended fix
fix.diff implements the finding's proposed fix:
sys/netproto/smb/smb_conn.h: add#define SMBIOD_EXITED 0x0002.sys/netproto/smb/smb_iod.cβsmb_iod_thread: immediately beforekthread_exit_compat(), setiod->iod_flags |= SMBIOD_EXITEDandwakeup(iod)β this is the exit barrier the destroyer waits on.sys/netproto/smb/smb_iod.cβsmb_iod_destroy: after the existing SYNCsmb_iod_request, loopwhile ((iod->iod_flags & SMBIOD_EXITED) == 0) tsleep(iod, ...)so the destroyer does notkfree(iod)until the kthread has actually exited. This mirrors the standard DragonFly pattern for kthread lifetime synchronization.
This matches the finding markdown's proposed fix (same flag, same exit-barrier, same destroyer wait loop).
Caveats
- The exact panic site observed (
smb_iod_request+0x58withiod=NULL) differs from the finding's specifically-cited stale-read site (smb_iod_thread:675/678). Both stem from the same iod-lifetime defect: the destroyer reclaimsiodwhile other code (either the iod kthread continuing pastwakeup(evp), or another VC-op caller reading a stalevcp->vc_iod) still expects it to be live. The proposed fix (wait forSMBIOD_EXITEDbeforekfree) closes the lifetime window for the destroyer-vs-kthread race. If a separate race exists onvcp->vc_ioditself (e.g., VC freed while another op readsvc_iod), that is a related-but-distinct refcount bug not covered by this fix. - Privileged trigger (PRIV_NETSMB required to open
/dev/nsmb). Not an unprivilegedβroot escalation. - Reliable panic reproduction: ~10-60 seconds of run time, four processes Γ 100 iterations each. The race is non-deterministic but strikes quickly under load.
Fix validation
Built a single-fix kernel (#1, Sun Jul 19 01:05:49 UTC 2026) with the
DF-0628 fix.diff applied (adds SMBIOD_EXITED flag, sets it in the iod
kthread before kthread_exit_compat(), and has smb_iod_destroy wait
for it before kfree(iod)). Ran the same PoC against the patched kernel:
| Kernel | Iter Γ Procs | Total cycles | Result |
|---|---|---|---|
| Baseline #0 (unpatched) | 100 Γ 4 | ~400 | panic at smb_iod_request+0x58 (iod=NULL, fault VA 0x58) |
| Patched #1 (fix applied) | 200 Γ 4 | 800 | no panic |
| Patched #1 (fix applied) | 500 Γ 6 | 3000 | no panic |
Fix confirmed: on the patched kernel, the destroyer waits for the iod
kthread to actually exit before freeing iod, closing the lifetime
window that produced the panic. 3000 cycles (7.5Γ the baseline panic
budget) completed without panic.
PoC files
args_overflow.cβ C PoC that triggers VC create/destroy in parallel.build.shβ compiles the PoC with the kernel-internal smb_dev.h header.run.shβ wraps the PoC; ensures smbfs.ko is loaded.run.logβ full run output (terminated by SSH connection loss as the guest entered DDB on panic).panic.txtβ the panic signature fromdfbsd-qemu/boot.log.boot.log.snapshotβ the full boot log up to and including the panic.build.logβ full build output.env.txtβ guest environment.fix.diffβ git-apply-able fix implementing the exit barrier.
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live panic). smb_iod_destroy kfree before kthread exits -> UAF/NULL-deref. Privileged (PRIV_NETSMB). Fix: SMBIOD_EXITED exit barrier.
No comments yet.