md_drvcleanup module-unload path has refcnt TOCTOU that frees md_s out from under a racing mdopen
Summary
On MOD_UNLOAD md_drvcleanup() does single unlocked atomic_fetchadd_int(&refcnt 0) read (:472) and if zero frees every md_s in mdlist (kfree(sc->secp) :492 kfree(sc) :493) without holding any lock mdopen/mdclose take. Concurrent mdopen() reads dev->si_drv1 then lwkt_gettoken(&sc->tok) (:133-134) and bumps refcnt (:137) can slip in between check and free dereferencing about-to-be-freed sc crashing/corrupting. dev->si_drv1 never NULLed before free. disk_destroy asynchronous so cdev and si_drv1 can still be referenced while sc freed. [VERIFIED pass2: reproduced - racing mdopen succeeds against the sc being freed, si_drv1 dangling, reclaim-read through dangling pointer returns 0xc7c7c7c7 UAF; destroy_dev-drain fix A/B EBUSY]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2477 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.0 KB | β raw | |
| VERDICT.md | β | 2.6 KB | β raw | |
| build.sh | β | 99 B | view raw | |
| run.log | β | 740 B | view raw | |
| env.txt | β | 963 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| manifest.json | β | 801 B | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-2477 -- md MOD_UNLOAD refcnt TOCTOU frees md_s under a racing mdopen
Verification used the shared rig tree at /root/rigs on the audit guest (see manifest). The rig for this finding: dfv_md (rig.c = harness, gen.sh = verbatim extractor, dfv_*_extract.h = generated production text). The decisive outputs below were captured live on the guest during the 2026-10-24 verification session; full session logs are summarized per run.
Build
sh build.sh # extracts verbatim driver text + builds the KLD
Run (guest, root)
kldload ./dfv_rig.ko
kldload dfv_md.ko; sysctl kern.dfv_md_run=1; sysctl -n kern.dfv_md_status
Expected
run.log: racing mdopen rc=0 refcnt=1 (open SUCCEEDED against an sc that was then freed); si_drv1 never NULLed; reclaim-read 0xc7c7c7c7; fixed: EBUSY, no frees in log, reclaim-read intact.
Fix validation
fix.diff drains the cdev (destroy_dev) and re-checks refcnt before any free; A/B on the same rig shows EBUSY and no free.
Verdict: reproduced / impact=corruption (see verdict.json, VERDICT.md)
DF-2477 verdict -- md MOD_UNLOAD refcnt TOCTOU frees md_s under a racing mdopen
status: reproduced reproduced: 1 impact: corruption confidence: certain attempts: 3 runtime_sec: 900
The verbatim md_drvcleanup (md.c:466-496) takes one unlocked atomic_fetchadd_int(&refcnt, 0) snapshot and then frees every md_s (kfree(sc->secp) :492, kfree(sc) :493) with no lock mdopen/mdclose hold (they take only sc->tok, which belongs to the freed object) and without clearing dev->si_drv1. The rig drives the exact interleaving: drvcleanup passes the refcnt==0 check, a racing verbatim mdopen completes at the drain boundary (rc=0, refcnt -> 1), then drvcleanup frees the md_s anyway. Result: refcnt==1 on freed memory, dev->si_drv1 dangling, and a read through the dangling pointer after a marker reclaim returns 0xc7c7c7c7 - use-after-free confirmed (the next mdopen/mdclose would take sc->tok on freed memory). md is compiled into the stock kernel (no unload at runtime here), so the trigger is module-config-specific, as filed. Fix v2 (destroy_dev drain first, then re-check refcnt, free only at 0) A/B: drvcleanup returns EBUSY, nothing freed, reclaim-read returns intact values (not the marker) - no UAF.
Evidence (decisive lines)
DF2477: racing mdopen rc=0 refcnt=1 (open SUCCEEDED against an sc that was then freed) DF2477: dev->si_drv1 was NEVER set to NULL after drvcleanup; read through the dangling pointer after reclaim: sc->unit=0xc7c7c7c7 sc->nsect=0xc7c7c7c7 (reclaimed marker 0xC7) DF2477: RESULT=UAF CONFIRMED (freed md_s reclaimed by unrelated allocation; next mdopen/mdclose takes sc->tok on freed memory) DF2477: log: mdcreate_malloc(auto-create) [after open+close refcnt=0] disk_destroy mdcreate_malloc(auto-create) [racing mdopen rc=0 refcnt=1] devstat_remove --- fixed source A/B --- DF2477: log: ... destroy_dev(drain) [racing mdopen rc=0 refcnt=1] (log ENDS here: EBUSY returned, nothing freed, sc intact - reclaim read returns non-marker values)
Exploit chain / privesc assessment
root-config path (module unload while opening); privesc ceiling: use-after-free on privileged-triggered unload; no unpriv trigger.
Fix + A/B
fix.diff drains the cdev (destroy_dev) and re-checks refcnt before any free; A/B on the same rig shows EBUSY and no free.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedConfirmed kernel references
Detail
Exploit chain
root-config path (module unload while opening); privesc ceiling: use-after-free on privileged-triggered unload; no unpriv trigger.
Evidence (decisive lines)
run.log: racing mdopen rc=0 refcnt=1 (open SUCCEEDED against an sc that was then freed); si_drv1 never NULLed; reclaim-read 0xc7c7c7c7; fixed: EBUSY, no frees in log, reclaim-read intact.
PoC changes
interleave implemented as a synchronous call at the interposed disk_destroy/destroy_dev boundary (single-threaded model of the two-CPU interleaving); first two designs (kthread stall) deadlocked and were replaced.
Verified recommended fix
destroy_dev (drain) first, then re-check refcnt before any free
Verdict
The verbatim md_drvcleanup (md.c:466-496) takes one unlocked atomic_fetchadd_int(&refcnt, 0) snapshot and then frees every md_s (kfree(sc->secp) :492, kfree(sc) :493) with no lock mdopen/mdclose hold (they take only sc->tok, which belongs to the freed object) and without clearing dev->si_drv1. The rig drives the exact interleaving: drvcleanup passes the refcnt==0 check, a racing verbatim mdopen completes at the drain boundary (rc=0, refcnt -> 1), then drvcleanup frees the md_s anyway. Result: refcnt==1 on freed memory, dev->si_drv1 dangling, and a read through the dangling pointer after a marker reclaim returns 0xc7c7c7c7 - use-after-free confirmed (the next mdopen/mdclose would take sc->tok on freed memory). md is compiled into the stock kernel (no unload at runtime here), so the trigger is module-config-specific, as filed. Fix v2 (destroy_dev drain first, then re-check refcnt, free only at 0) A/B: drvcleanup returns EBUSY, nothing freed, reclaim-read returns intact values (not the marker) - no UAF.
No comments yet.