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)
PoC verification
Evidence pack
findings/poc/DF-4466 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedfix.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.
Confirmed kernel references
- sys/dev/disk/dm/crypt/dm_target_crypt.c:693
- sys/dev/disk/dm/crypt/dm_target_crypt.c:750
- sys/dev/disk/dm/crypt/dm_target_crypt.c:607
- sys/dev/disk/dm/crypt/dm_target_crypt.c:976
- sys/dev/disk/dm/device-mapper.c:408
- sys/dev/disk/dm/device-mapper.c:550
- sys/dev/disk/dm/dm_ioctl.c:440
- sys/dev/disk/dm/dm_ioctl.c:508
- sys/dev/disk/dm/dm_table.c:129
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
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.
No comments yet.