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

dm-crypt: use-after-free of target config - async completions race table destroy (suspend/reload/resume and remove paths have no in-flight drain)

Summary

strategy() stores only a raw priv pointer in each bio and the DM framework drops its table reference as soon as strategy() returns; crypt completions run asynchronously long afterwards holding nothing but that pointer. Suspend is a flag-only no-op and resume/remove destroy the active table with no drain - destroy mpipe_dones and zero-wipes priv while bios are in flight. Completion then dereferences freed memory (NULL write via zero-wiped chunk; indirect call priv->ivgen->gen_iv + forged-session crypto on a reclaimed chunk - kernel PC control primitive). REPRODUCED deterministically 3/3 via crypt->delay(12s)->vn0 + fast-table reload (Fatal trap 12 in mpipe_alloc_callback); hang manifestation also demonstrated. Root-triggerable via routine LVM suspend+reload+resume on a busy crypt volume. Fix (per-bio nactive drain) built and validated on the guest: panic gone, I/O completes.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4466 Β· 13 files
FileTypeDescriptionSize
dmrace.c β€” 18.1 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
run.log β€” 2.9 KB view raw
fix_run.log β€” 1.6 KB view raw
fix_build.log β€” 5.6 MB ↓ download
fix.diff β€” 2.8 KB view raw
panic.txt β€” 661 B view raw
ps_hang_variantD.txt β€” 898 B view raw
dmesg_variantD.txt β€” 1.8 KB view raw
env.txt β€” 199 B view raw
manifest.json β€” 904 B view raw
verdict.json β€” 3.9 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in-guest, nativekernel+installkernel, rebooted: exact F0 PoC no longer panics - resume now blocks ~11s draining the in-flight read (tsleep dmtcdn), read returns correct plaintext 0x9e111cd8 errno=0, config freed afterwards; variant B (36k writes, 10 reload cycles) clean; guest stable.

fix_run.log (patched timeline: resume 13.0s->24.1s, reader correct), fix_build.log (full build, DONE=0), /boot/kernel/dm_target_crypt.ko contains dmtcdn ident.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Sep 20 13:35:38 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

priv UAF: (1) crypt->delay(12s) stack parks a read bio; (2) suspend+reload-with-fast-table+resume frees old config immediately (dm_table_destroy, no drain); (3) completion derefs freed config: zero-wiped chunk gives NULL-page write (observed panic); reclaimed chunk gives mpipe_alloc_callback on controlled mpipe then priv->ivgen->gen_iv indirect call and forged crypto session -> kernel code execution on the no-SMAP/SMEP/KASLR guest. Full uid=0 chain not completed (trigger itself requires /dev/mapper/control = root); no structural blocker documented in VERDICT.md.

Evidence (decisive lines)

run.log + panic.txt: Fatal trap 12, Stopped at mpipe_alloc_callback+0x1a2 (3/3 from fresh resets); ps_hang_variantD.txt: writers in D forever; fix_run.log: patched kernel drains (resume blocks 11s) and read returns correct 0x9e111cd8, no panic; VERDICT.md timeline.

PoC changes

Replaced the seeded sketch with a proplib-XML ioctl client (plistref ABI requires wrapper, hex integers); added warm-up read (open-time label probe eats one delay round); variant F reloads a FAST table to bypass the accidental disk_config() probe-drain that shields naive reload reproducers; 6 variants (A-F) developed to characterize which destroy paths race.

Verified recommended fix

Per-bio in-flight reference on the crypt config (nactive) incremented in strategy under the table lock and dropped at every completion endpoint; destroy drains nactive before mpipe/session teardown.

Verdict

Deterministic use-after-free (3/3 kernel panics, Fatal trap 12 supervisor write to 0x0 in mpipe_alloc_callback+0x1a2) plus permanent in-flight-bio hangs: dm-crypt completions run asynchronously holding only bio_caller_info1.ptr while suspend(no-op)/reload/resume destroys the table config with no drain. Root-triggerable via the routine LVM reload flow on busy volumes; freed config is zero-wiped (NULL write) or attacker-reclaimable (priv->ivgen->gen_iv indirect call) - memory-corruption class.