dm_table_load_ioctl dereferences uninitialized stack pointer `type` as a C string (strcmp/ksnprintf wild read -> kernel panic)
Summary
`type` is declared but never initialized (unlike name/uuid/str); get_cstring_nocopy returns false WITHOUT touching the out-param on a missing/mistyped DM_TABLE_TYPE, and the stale stack value is then strcmp'd by dm_target_lookup and ksnprintf'd by the autoload path. Proven on guest: groom mode 250/250 rounds (ENOENT+table-wipe only reachable through the wild lookup), flood mode panicked (Fatal trap 12 at strcmp+0xc) with garbage autoload names on console. Operator-group trigger (mapper/control 0640). Wild read only - same ceiling as DF-2448. Init-and-check-the-return fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4472 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dm_uninit_type.c | β | 8.6 KB | view raw | |
| build.sh | β | 109 B | view raw | |
| run.sh | β | 130 B | view raw | |
| run.log | β | 3.7 KB | view raw | |
| panic.txt | β | 2.7 KB | view raw | |
| fix.diff | β | 4.3 KB | view raw | |
| env.txt | β | 414 B | view raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| README.md | β | 1.1 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-4472 PoC β uninitialized type pointer in dm_table_load_ioctl
Build (guest, after vm.sh reset with-src + kldload dm):
cc -o dm_uninit_type dm_uninit_type.c -lprop -lpthread
Run:
./dm_uninit_type # groom mode: 200 rounds of [valid reload, typeless reload]
./dm_uninit_type --flood 8 800 # 8 threads x 800 malformed reloads (panic variant)
Expected (unpatched) β any of:
- groom:
destroy-proof=N== rounds β every typeless reload returns ENOENT (rv2=2) and wipes the inactive table loaded by the paired valid reload, provingdm_target_lookup(garbage)executed (dm_ioctl.c:741 is the only ENOENT return after the destroy at :722-723); - flood: kernel panic β
Fatal trap 12,Stopped at strcmp+0xc(wild read of the uninitialized pointer), guest down; - possibly
dm: could not autoload module for target <garbage>on console.
Expected (patched with fix.diff): groom prints last rv2=22 (EINVAL)
with destroy-proof=0; flood completes without panic.
Requires root (or operator group): /dev/mapper/control is 0640 root:operator.
DF-4472 β dm_table_load_ioctl: uninitialized stack pointer type dereferenced as a C string
REPRODUCED (panic / local DoS) + FIX VALIDATED.
The bug
dm_table_load_ioctl() (sys/dev/disk/dm/dm_ioctl.c:673) declares
const char *name, *uuid, *type; at :684 and initializes name/uuid/dmv/str β
but never type (:690-695). Inside the table-entry loop it does:
730 while ((target_dict = prop_object_iterator_next(iter)) != NULL) {
731 prop_dictionary_get_cstring_nocopy(target_dict, DM_TABLE_TYPE, &type);
...
737 if (((target = dm_target_lookup(type)) == NULL) &&
738 ((target = dm_target_autoload(type)) == NULL)) {
prop_dictionary_get_cstring_nocopy() (sys/libprop/prop_dictionary_util.c:185)
returns false without touching the out-param when the key is absent, the
element is not a dictionary, or the value is not a string. On the first loop
iteration type therefore holds stale kernel-stack data, which is then
dereferenced as a NUL-terminated string by:
dm_target_lookup_name()βstrcmp(dm_target_name, dmv->name)(sys/dev/disk/dm/dm_target.c:127) β wild read;- on lookup failure,
dm_target_autoload()βksnprintf(mod_name, sizeof, "dm_target_%s", dm_target_name)(dm_target.c:74) β second wild read of the same garbage.
Reproduction (baseline, unpatched)
Three independent confirmations on the QEMU guest (DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, dm.ko kldload-ed):
- Wild read executes on 100% of rounds (250/250). Groom mode: reload #1
(valid,
type:"zero") loads a 1-entry inactive table; reload #2 omits the"type"key entirely β returns ENOENT and the inactive table is now empty. dm_ioctl.c has exactly one ENOENT return after thedm_table_destroy(INACTIVE)at :722-723 β the lookup failure at :741 β sodm_target_lookup(garbage)demonstrably executed with the stale value (run.log: 50/50 and 200/200 rounds). - Autoload prints the garbage. Serial console during flood:
dm: could not autoload module for target <binary garbage>β the ksnprintf at dm_target.c:74 reading uninitialized memory. - PANIC. Flood mode (8 threads Γ 800 malformed reloads) β the garbage
landed on an unmapped value (fault VA 0x8) and the kernel took a fatal
page fault in
strcmp+0xccalled from the lookup path; guest down (panic.txt:Fatal trap 12 β¦ Stopped at strcmp+0xc: movzbl (%rdi,%rax,1),%edx).
Why no uid=0
The primitive is a wild read of a stack-derived pointer used as a string. There is no write primitive; the value at the stack slot cannot be aimed at an attacker-chosen address by construction (it is kernel-stack residue from the proplib internalize machinery of the same ioctl). Best case for an attacker is the observed reliable local DoS (panic) β the same ceiling as the known DF-2448 NULL-deref on this surface. Valid hard blocker for escalation.
Privilege precondition
/dev/mapper/control is created 0640 root:operator
(sys/dev/disk/dm/device-mapper.c:181) and dm must be kldload-ed by root β
threat model is operator-group member (or root) β kernel, consistent with the
known DF-1639/1638xx family on this file.
Fix validation
fix.diff (shared with DF-4473/DF-4474; this file's relevant hunk):
initialize type = NULL before the loop, re-initialize per iteration, and
check the prop_dictionary_get_cstring_nocopy return value β EINVAL.
- Baseline: groom 50/50 wild-read proofs; flood β fatal trap 12, guest down.
- Patched (dm.ko rebuilt with fix.diff, -Werror clean): groom returns EINVAL (22) on every round (missing type rejected before any lookup); flood 8Γ800 completes with no panic, guest healthy (run.log Β§fix).
Files
dm_uninit_type.cβ PoC (groom + flood modes)run.logβ baseline groom (250/250), baseline panic, patched groom + floodpanic.txtβ full serial-console panic excerpt (strcmp+0xc, trap 12)fix.diffβ combined dm_ioctl.c fix (validated)env.txt,build.sh,run.sh
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
fixedBaseline: groom proved the wild read on 250/250 rounds and flood panicked the guest (trap 12, strcmp+0xc). Patched (dm.ko rebuilt from fix.diff, -Werror clean, kldload-ed): groom returns EINVAL(22) every round (type checked before lookup), flood 8 threads x 800 completes with no panic, guest healthy.
run.log fix-validation section; fix.diff (combined dm_ioctl.c diff, hunk adding type=NULL + checked prop_dictionary_get_cstring_nocopy)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (groom 50/50 + 200/200 destroy-proof; flood -> panic; patched: rv2=22, no panic); panic.txt (Fatal trap 12, Stopped at strcmp+0xc: movzbl (%rdi,%rax,1),%edx; 'dm: could not autoload module for target <garbage>'); fix.diff + fix validation section of run.log
PoC changes
Authored fresh (pass-2 finding, no seed). Two modes: groom (valid reload then typeless reload + inactive-table query via table ioctl with DM_QUERY_INACTIVE_TABLE_FLAG) and flood (multi-threaded malformed reloads with padding entries to vary stack residue). Added pre-round verification that reload#1 actually loaded (defends against the trivial empty-table explanation).
Verified recommended fix
Initialize type=NULL before the loop and per iteration; reject entries whose DM_TABLE_TYPE is missing/not a string with EINVAL instead of looking up the stale pointer.
Verdict
Uninitialized stack pointer type (never initialized at dm_ioctl.c:684/690-695; prop_dictionary_get_cstring_nocopy at :731 leaves it untouched when the "type" key is absent) is dereferenced as a C string by dm_target_lookup (strcmp, dm_target.c:127) and dm_target_autoload (ksnprintf, dm_target.c:74). Verified three ways on the guest: (1) groom mode proves the wild lookup executed on 250/250 rounds (typeless reload returns the :741 ENOENT and wipes the inactive table destroyed at :722-723 -- the only ENOENT return after that point); (2) console printed the garbage string via the autoload ksnprintf; (3) flood mode produced a fatal trap 12 page fault at strcmp+0xc (fault VA 0x8) and killed the guest. Ceiling is a reliable local DoS (panic) for an operator-group user -- wild read, no write primitive, so no escalation chain (hard blocker). Fix (type=NULL init + checked get_cstring_nocopy -> EINVAL) validated on a rebuilt dm.ko: groom returns EINVAL, flood survives 8x800 with no panic.
No comments yet.