β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2477

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2477 Β· 8 files
FileTypeDescriptionSize
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)

VERDICT.md
↓ download raw

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

fixed
↓ fix.diffper-fix-DF-2477

Confirmed 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.