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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1640 Β· 14 files
FileTypeDescriptionSize
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
README.md readme double-free mechanism + INVARIANTS analysis
↓ download 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 at chunk_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 the M_TEMP kmalloc-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 dm KLD 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)

  1. Iteration 1 of the while loop (dm_ioctl.c:730): entry has params="AAAA...". - dm_ioctl.c:771 get_cstring succeeds β†’ str = kmalloc'd "AAAA..." (17 bytes, M_TEMP kmalloc-32 bucket). - dm_ioctl.c:783 dm_table_init β€” zero target has no ->init, so str is ignored, ret=0. - dm_ioctl.c:791 kfree(str, M_TEMP) β€” frees the chunk. str is now dangling.
  2. Iteration 2: entry has no params key. - dm_ioctl.c:771 get_cstring fails β†’ str left unchanged (still dangling). - dm_ioctl.c:791 kfree(str, M_TEMP) β†’ double-free of the same chunk. - chunk_mark_free sees 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 kfree calls 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-syscall kmalloc-32/M_TEMP spray 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: panic on GENERIC (valid INVARIANTS blocker). No uid0 on 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.

VERDICT.md verdict REPRODUCED, write-class primitive neutralized by INVARIANTS on GENERIC
↓ download raw

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_TEMP string (kmalloc-32 bucket).
  • On GENERIC (INVARIANTS ON): the second kfree trips chunk_mark_free's already-free check and panics immediately. The two kfree calls 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-syscall M_TEMP/kmalloc-32 spray could reclaim it into two objects β†’ type confusion β†’ potential uid0. This is a non-default-kernel escalation path.
  • Outcome: panic on default GENERIC (valid INVARIANTS blocker). No uid0 on 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

fixed

validated

baseline panic; patched returns EINVAL/ENOTSUP, guest up
↓ fix.diffdm.ko/dm_target_crypt.ko module rebuild atop 6.5-DEVELOPMENT #0

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.