Use-after-free + double-free of DM_TABLE_PARAMS string across table entries
Summary
dm_ioctl.c:688-695 str=NULL before loop. 771 prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str) only writes *cpp on success; on fail str stays stale. 791 kfree(str,M_TEMP) unconditional. Next iteration without params -> str is dangling -> dm_table_init:827 strsep on freed memory (UAF write) -> kfree(str) double-free. Trigger: reload with cmd_data=[{type=zero,params=...},{type=zero}] (second entry omits params). Operator group. Heap corruption M_TEMP. Fix: str=NULL when get_cstring fails + only kfree if str!=NULL.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1640 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dm_poc.c | trigger-source | case 1640: reload with [zero(params), zero(no params)] | 10.5 KB | view raw |
| build.sh | build-script | cc -o dm_poc dm_poc.c | 117 B | view raw |
| run.sh | run-script | ./dm_poc create; ./dm_poc 1640 | 256 B | view raw |
| README.md | readme | double-free mechanism + INVARIANTS analysis | 3.7 KB | β raw |
| VERDICT.md | verdict | REPRODUCED, write-class primitive neutralized by INVARIANTS on GENERIC | 3.6 KB | β raw |
| build.log | build-log | PoC compile output | 98 B | view raw |
| run.log | run-log | baseline double-free panic + fix clean return | 501 B | view raw |
| fix_build.log | fix-build-log | patched dm.ko module build output | 1.0 KB | view raw |
| fix_run.log | fix-run-log | patched-module test: EINVAL, no double-free | 199 B | view raw |
| panic.txt | panic-signature | panic: memory chunk already free! in chunk_mark_free via _kfree | 446 B | view raw |
| fix.diff | suggested-fix | reset str=NULL each iteration + guard kfree with NULL check | 1.0 KB | view raw |
| env.txt | environment | uname, cc, module list, test user | 809 B | view 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-1640 β Use-after-free + double-free of DM_TABLE_PARAMS string across table entries
Summary
dm_ioctl.c:688 declares char *str; and initializes it to NULL at line 695
(before the table-entry loop). Inside the loop, line 771
prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str) only writes *cpp
on success (confirmed in sys/libprop/prop_dictionary_util.c:185-198: on failure
the output pointer is left untouched). Line 791 kfree(str, M_TEMP) frees
unconditionally. So on a second table entry that omits params, str still holds
the dangling pointer freed in iteration 1 β kfree frees it again β
double-free in M_TEMP slab zone.
Severity / impact
- Severity filed: High
- Verified impact:
panic(local DoS) on the default GENERIC kernel (X86_64_GENERIC, INVARIANTS ON). The slab allocator's INVARIANTS check atchunk_mark_free(kern_slaballoc.c) catches the double-free immediately and panics before any heap grooming can land. - Primitive class: write (double-free / slab freelist corruption) β but on
GENERIC with INVARIANTS ON, the corruption never lands (caught at the second
kfree). On an INVARIANTS-OFF (noinv) kernel the double-free would silently corrupt theM_TEMPkmalloc-32 freelist; that is a non-default-kernel result. The INVARIANTS trip is a valid blocker for default-GENERIC escalation. - Trigger credential: operator group.
- Precondition: admin has loaded
dmKLD module + a dm device must exist.
Reproduce
kldload dm
pw groupmod operator -m <user>
./build.sh
./dm_poc create # helper: create device "pocdev"
./run.sh # reload with two entries: 1st has params, 2nd omits params
# expected (BUG): panic: memory chunk ... is already free!
# expected (FIXED): EINVAL, guest stays up
Mechanism (line-accurate)
- Iteration 1 of the
whileloop (dm_ioctl.c:730): entry hasparams="AAAA...". -dm_ioctl.c:771get_cstringsucceeds βstr= kmalloc'd "AAAA..." (17 bytes,M_TEMPkmalloc-32 bucket). -dm_ioctl.c:783dm_table_initβ zero target has no->init, so str is ignored, ret=0. -dm_ioctl.c:791kfree(str, M_TEMP)β frees the chunk. str is now dangling. - Iteration 2: entry has no
paramskey. -dm_ioctl.c:771get_cstringfails βstrleft unchanged (still dangling). -dm_ioctl.c:791kfree(str, M_TEMP)β double-free of the same chunk. -chunk_mark_freesees the chunk is already marked free βpanic: memory chunk 0x... is already free!
Exploit-chain assessment (write-class primitive)
- Bucket:
M_TEMP/ kmalloc-32 (17-byte string). - The two
kfreecalls happen in the same syscall, iterations of the same loop, with no scheduler intervention between them. There is no reclamation window to groom a victim object between the first and second free. - On
noinv(INVARIANTS OFF), the double-free would add the chunk to the freelist twice; a subsequent cross-syscallkmalloc-32/M_TEMPspray could reclaim it into two objects β type confusion. This is a non-default-kernel escalation path (INVARIANTS OFF). On default GENERIC it is a DoS (INVARIANTS catches it). - Outcome:
panicon GENERIC (valid INVARIANTS blocker). Nouid0on default kernel.
Fix
fix.diff: (1) reset str = NULL at the start of each loop iteration before
get_cstring; (2) guard both kfree(str) sites with if (str != NULL). Matches
the finding's proposed fix.
Fix validation
Patched dm.ko (combined with DF-1639+DF-1642 fixes), re-ran PoC: returns EINVAL
(the second entry's dm_table_init gets str=NULL β returns EINVAL at
dm_table.c:814), no double-free, guest stays up.
DF-1640 β VERDICT
Verdict: REPRODUCED (panic / local DoS via slab double-free)
Root cause
sys/dev/disk/dm/dm_ioctl.c:771-791:
/* declared before loop, init NULL at :695 */
char *str;
...
while ((target_dict = prop_object_iterator_next(iter)) != NULL) {
...
prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str); /* :771 */
...
if ((ret = dm_table_init(target, table_en, str)) != 0) {
...
kfree(str, M_TEMP); /* :786 */
...
}
kfree(str, M_TEMP); /* :791 -- unconditional */
}
prop_dictionary_get_cstring (confirmed at sys/libprop/prop_dictionary_util.c:185-198)
writes *cpp only on success. On a table entry that omits params, the call
fails and str retains its previous value. After iteration 1 freed str at :791,
iteration 2 (no params) reuses the dangling pointer and kfrees it again β
double-free in M_TEMP kmalloc-32.
Evidence (baseline, unpatched dm.ko)
panic: memory chunk 0xfffff8008d580dd0 is already free! cpuid = 2 chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe _kfree() at _kfree+0x262 0xffffffff806580e2 Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip) db>
The chunk_mark_free INVARIANTS check (kern_slaballoc.c) catches the double-free
and panics before any heap grooming can land. Triggered by operator-group maxx:
create device, then reload with cmd_data=[{zero,params=AAAA...},{zero,no params}].
Exploit-chain assessment
- Primitive: double-free of a 17-byte
M_TEMPstring (kmalloc-32 bucket). - On GENERIC (INVARIANTS ON): the second
kfreetripschunk_mark_free's already-free check and panics immediately. The twokfreecalls are in the same syscall (consecutive loop iterations) with no scheduler intervention between them, so there is no reclamation window to groom a victim object between frees. - On
noinv(INVARIANTS OFF): the double-free would silently corrupt the slab freelist (chunk appears twice); cross-syscallM_TEMP/kmalloc-32 spray could reclaim it into two objects β type confusion β potentialuid0. This is a non-default-kernel escalation path. - Outcome:
panicon default GENERIC (valid INVARIANTS blocker). Nouid0on default kernel. The write-class primitive is confirmed but neutralized by INVARIANTS on the realistic target.
PoC changes
Authored dm_poc.c from scratch. The 1640 case crafts a reload with two table
entries: the first has params="AAAAAAAAAAAAAAAA" (17 bytes β kmalloc-32), the
second omits params entirely, exercising the stale-str β double-free path.
Fix (fix.diff)
+ str = NULL;
prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str);
...
- kfree(str, M_TEMP); /* error path */
+ if (str != NULL)
+ kfree(str, M_TEMP);
...
- kfree(str, M_TEMP); /* normal path */
+ if (str != NULL)
+ kfree(str, M_TEMP);
Matches the finding's proposed fix (str=NULL when get_cstring fails + only kfree if
str!=NULL). Resetting str=NULL each iteration breaks the stale-pointer chain;
the NULL guards make both kfree sites safe.
Fix validation
Patched dm.ko, re-ran PoC:
DF-1640 reload(double-free str): rc=-1 errno=22 (Invalid argument) EXIT=0
With the fix, the second entry's dm_table_init(target, table_en, NULL) returns
EINVAL at dm_table.c:814 (if (params == NULL) return EINVAL), the error path
returns cleanly, and no double-free occurs. Guest stayed up. fix_status = fixed.
Fix verification
fixedvalidated
baseline panic; patched returns EINVAL/ENOTSUP, guest up
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live panic). dm reload stale str double-free -> chunk_mark_free INVARIANTS panic. Operator-group.
No comments yet.