dm_message_ioctl frees and dereferences an uninitialized stack msg pointer
Summary
dm_message_ioctl declares char *msg with no initializer and calls prop_dictionary_get_cstring without checking return. proplib leaves *cpp unwritten when key missing or wrong-typed. If attacker omits message key msg stays random stack garbage passed to table_en->target->message(table_en msg) and then unconditionally kfree(msg M_TEMP) - kernel arbitrary-free/NULL-adjacent-deref of attacker-influenced pointer. Also sector uninitialized when DM_MESSAGE_SECTOR absent used in sector==0 extent-match branch. Unguarded kfree executes on every path after successful dm_dev_lookup so any existing dm device with active table triggers it. Attacker: operator group with /dev/mapper/control. Reliable kernel panic with heap grooming controlled double-free/arbitrary free.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2446 · 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dm_uninit_msg.c | trigger-source | create dm device + message ioctl omitting 'message' key -> kfree of uninitialized stack ptr | 4.6 KB | view raw |
| build.sh | build-script | cc -o dm_uninit_msg dm_uninit_msg.c -lprop | 176 B | view raw |
| run.sh | run-script | kldload dm; ./dm_uninit_msg | 274 B | view raw |
| run.log | run-log | baseline (unpatched) run + panic signature | 1.6 KB | view raw |
| fix_run.log | run-log | patched dm.ko run -> clean EINVAL, guest up | 1.3 KB | view raw |
| panic.txt | panic-signature | fatal trap 12 page fault at _kfree+0x45, fault addr 0x2e2e7a4a7054 | 425 B | view raw |
| boot_unpatched.log | serial-log | full serial console of the panicking run | 13.8 KB | view raw |
| boot_patched.log | serial-log | serial console of the patched run (no panic) | 122 B | view raw |
| fix.diff | suggested-fix | init msg=NULL, check prop_dictionary_get_cstring return, guard kfree | 812 B | view raw |
| fix_build.log | build-log | full nativekernel build log (proves fix compiles into kernel + dm.ko) | 5.6 MB | ↓ download |
| dm_module_build.log | build-log | standalone dm module rebuild log (dm.ko used for validation) | 220 B | view raw |
| env.txt | environment | uname, cc version, kern.version, dm.ko sha256 | 547 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, primitive, privilege analysis, fix validation | 8.4 KB | ↓ raw |
| README.md | readme | human-readable summary + reproduce instructions | 2.3 KB | ↓ raw |
DF-2446 — dm_message_ioctl uninitialized msg free/deref
Summary
dm_message_ioctl() (sys/dev/disk/dm/dm_ioctl.c) declares char *msg
without an initializer and calls prop_dictionary_get_cstring(...,&msg)
without checking the return. When the "message" key is missing from the
ioctl dictionary, proplib leaves *cpp unwritten, so msg holds stack
residue. The function then calls kfree(msg, M_TEMP) on that residue →
kernel page-fault / panic (or, with stack grooming, an arbitrary-free/UAF
primitive).
Privilege
Root/operator-only. /dev/mapper/control is 0640 root:operator
(device-mapper.c:181), the dm module is demand-loaded via root-only
kldload, and there is no setuid helper or devfs relaxation. Verified:
unprivileged maxx gets Permission denied. This is a root→kernel
robustness/hardening gap (local DoS + potential heap corruption), not
an unprivileged→root escalation.
Reproduce
./build.sh && ./run.sh # run.sh does: kldload dm; ./dm_uninit_msg
- Build:
cc -o dm_uninit_msg dm_uninit_msg.c -lprop - Run as root (must be root or operator-group to open the control dev).
- Expected on the BUGGY (unpatched) kernel: kernel panic —
Fatal trap 12: page fault while in kernel mode,Stopped at _kfree+0x45: movl 0x54(%rax),%r13d, guest wedged in DDB. - Expected on the FIXED kernel: the message ioctl returns
EINVAL (22), the PoC printsmessage ioctl returned rv=22, exits 0, and the guest stays up.
How the PoC works
- Opens
/dev/mapper/control. - Sends
NETBSD_DM_IOCTLwithcommand="create",name="df2446dev"— creates a dm device sodm_dev_lookup()succeeds insidedm_message_ioctl. - Sends
NETBSD_DM_IOCTLwithcommand="message",name="df2446dev", and omits the"message"key.prop_dictionary_get_cstringreturns false without writing&msg, somsgstays uninitialized; the unconditionalkfree(msg, M_TEMP)frees stack garbage.
Fix
See fix.diff: initialize char *msg = NULL;, check the
prop_dictionary_get_cstring return (return EINVAL on failure after
unbusying the device), and guard the cleanup kfree with if (msg != NULL).
Validated by rebuilding the dm module and re-running the same PoC —
panic → clean EINVAL.
DF-2446 — dm_message_ioctl uninitialized msg free/deref
Verdict
REPRODUCED (panic / local DoS) + FIX VALIDATED. The bug is real and
deterministically crashes the kernel. The escalation to uid=0 is blocked
by a valid hard blocker: the vulnerable ioctl path is reachable only from
an already-root context (kldload + a 0640 root:operator device node), so
there is no privilege boundary to cross — this is a root→kernel
robustness/hardening gap, not an unprivileged→root escalation. The
authored fix.diff is built, installed as dm.ko, and confirmed to close
the bug (panic → clean EINVAL).
Mechanism (trigger → primitive → effect)
dm_message_ioctl() in sys/dev/disk/dm/dm_ioctl.c has an uninitialized
auto pointer that is freed unconditionally:
997: int
998: dm_message_ioctl(prop_dictionary_t dm_dict)
999: {
1000: ...
1006: char *msg; <-- UNINITIALIZED
1007: int ret, found = 0;
...
1022: if ((dmv = dm_dev_lookup(name, uuid, minor)) == NULL) {
1023: dm_remove_flag(dm_dict, &flags, DM_EXISTS_FLAG);
1024: return ENOENT; <-- must pass: device must exist
1025: }
1026:
1027: /* Get message string */
1028: prop_dictionary_get_cstring(dm_dict, DM_MESSAGE_STR, &msg);
<-- RETURN VALUE NOT CHECKED
...
1058: kfree(msg, M_TEMP); <-- frees stack garbage
1059: dm_dev_unbusy(dmv);
prop_dictionary_get_cstring (sys/libprop/prop_dictionary_util.c:185) is
documented and implemented to leave *cpp unwritten when the key is
missing or the value is not a string:
185: prop_dictionary_get_cstring(prop_dictionary_t dict, const char *key, char **cpp)
192: if (prop_object_type(str) != PROP_TYPE_STRING)
193: return (false); /* does NOT write *cpp */
So if the ioctl dictionary carries command="message" plus a valid device
name (so dm_dev_lookup succeeds) but omits the "message" key, msg
keeps whatever stack residue the frame holds, and kfree(msg, M_TEMP) is
called on that residue.
Confirmed effect (unpatched #0 kernel)
Fatal user address access from kernel mode from dm_uninit_msg at ffffffff80657ec5 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x2e2e7a4a7054 fault code = supervisor read data, page not present instruction pointer = 0x8:0xffffffff80657ec5 current process = 987 Stopped at _kfree+0x45: movl 0x54(%rax),%r13d db>
msg happened to hold 0x2e2e7a4a7000 (stack residue; the 0x2e bytes are
ASCII . from prior proplib XML buffers). _kfree dereferences the chunk
header at rax+0x54 to read slab metadata → page fault on an unmapped
address → fatal trap 12 → kernel panic, guest wedged in DDB.
Primitive characterization
- Class: free of an uninitialized (stack-residue) pointer.
- Attacker control of the freed address: indirect. The pointer is stack residue, not directly settable via the ioctl arguments, but it can be influenced by prior stack frames in the same syscall path (proplib externalize/internalize buffers, prior ioctl dispatch frames). With stack grooming (a controlled prior call sequence) the residue could be steered toward a valid slab address, turning this into an arbitrary-free / UAF.
- Observed outcome: deterministic kernel panic (DoS). On this guest the residue was unmapped, so it manifested as a page fault rather than silent heap corruption.
Why no uid=0 chain (valid hard blocker)
Per the Phase-6 hard-blocker rules, the escalation chain is blocked because the vulnerable write is reachable only from an already-root context:
- Module load: the
dmdriver is a KLD module (DECLARE_MODULE(dm, …)indevice-mapper.c:97); it is not built into the GENERIC kernel and is not auto-loaded. Reaching the ioctl requireskldload dm, which is a root-only operation (priv_checkonPRIV_KLD_LOAD). - Device node permission: the control device is created as
make_dev(&dmctl_ops, 0, UID_ROOT, GID_OPERATOR, 0640, "mapper/control")(device-mapper.c:181) — i.e.crw-r----- root operator. - Unprivileged user cannot open it: verified on the guest —
maxx(uid 1001, gid 1001, not inoperatororwheel) getsopen /dev/mapper/control: Permission denied. There is no devfs rule in/etc/devfs.confor/etc/defaults/devfs.confthat relaxes this, anddmsetup/lvmare not setuid (-r-xr-xr-x root wheel).
Root→kernel is game-over by definition (root can already set uid=0), so
there is no privilege boundary for this bug to cross. Realistic impact
ceiling: a root operator (or any operator-group member) can
deterministically panic/crash the kernel (local DoS), and — with stack
grooming — potentially corrupt the kernel heap. This is a defense-in-depth
/ robustness fix worth making, not a privilege-escalation finding.
PoC
dm_uninit_msg.c — uses libprop to issue two NETBSD_DM_IOCTL ioctls:
1. command="create", name="df2446dev" → creates a dm device so
dm_dev_lookup succeeds.
2. command="message", name="df2446dev", "message" key omitted →
triggers the uninitialized kfree.
Build: cc -o dm_uninit_msg dm_uninit_msg.c -lprop
Run (as root): kldload dm && ./dm_uninit_msg
Fix (fix.diff)
Minimal, targeted at the root cause:
char *msg = NULL;— deterministic initialization.- Check the
prop_dictionary_get_cstringreturn; on failure (key missing / wrong type)dm_dev_unbusy(dmv); return EINVAL;— never reachkfreewith an uninitialized pointer. - Defensive
if (msg != NULL) kfree(msg, M_TEMP);at the cleanup site (now always-true, but guards future regressions).
git apply --check passes against the read-only sys/ tree. Applied to
in-guest /usr/src, the dm module was rebuilt and installed as
/boot/kernel/dm.ko (sha256 358587fc…); the same PoC that panicked the
unpatched kernel now returns EINVAL (22) and the guest stays up.
Fix validation (Phase 8)
| step | result |
|---|---|
baseline #0 kernel |
panic at _kfree+0x45, fault 0x2e2e7a4a7054, guest DDB |
apply fix.diff to /usr/src |
3 hunks applied cleanly |
| rebuild dm module | make in sys/dev/disk/dm → dm.ko OK, no errors |
| install dm.ko | /boot/kernel/dm.ko replaced (kernel image left at #0) |
| re-run PoC (×2) | rv=22 (EINVAL), kernel survives, guest up, no panic |
(kernel image left at the working #0 baseline because dm is a purely
loadable module — the fix lives entirely in dm.ko, not in the kernel
proper. The first nativekernel rebuild also succeeded (NK_DONE rc=0) and
produced an equivalent patched dm.ko; the standalone module build was used
for the final validation to avoid an unnecessary kernel-image swap.)
Files
| file | purpose |
|---|---|
dm_uninit_msg.c |
trigger PoC (create device + message ioctl w/o message key) |
build.sh |
cc -o dm_uninit_msg dm_uninit_msg.c -lprop |
run.sh |
kldload dm && ./dm_uninit_msg |
run.log |
baseline (unpatched) run + panic signature |
fix_run.log |
patched dm.ko run → clean EINVAL, guest up |
panic.txt |
fatal-trap 12 signature from boot.log |
boot_unpatched.log |
full serial log of the panicking run |
boot_patched.log |
serial log of the patched run (no panic) |
fix.diff |
git-apply-able fix (init msg, check return, guard kfree) |
fix_build.log |
full nativekernel build log (proves fix compiles) |
dm_module_build.log |
standalone dm module rebuild log |
env.txt |
guest uname / cc / kern.version / dm.ko hash |
manifest.json |
machine-readable artifact catalog |
Fix verification
fixedVALIDATED: PoC on unpatched dm.ko baseline panics deterministically (Stopped at _kfree+0x45, guest DDB). After fix.diff + rebuilt dm.ko, SAME PoC returns rv=22 EINVAL, exits 0, guest up, no panic over two runs. Uninitialized-pointer kfree gone; bug closed.
baseline (dm.ko b9084b66): Fatal trap 12 page fault, Stopped at _kfree+0x45: movl 0x54(%rax),%r13d, guest DDB, vm down. patched (dm.ko 358587fc): message ioctl returned rv=22 (EINVAL), RUN_EXIT=0, vm up, boot.log clean.
Confirmed kernel references
Detail
Exploit chain
BLOCKED by a valid Phase-6 hard blocker: root-only reachability. dm is a demand-loaded KLD (not in GENERIC; needs kldload dm = PRIV_KLD_LOAD root-only); control device is make_dev(UID_ROOT, GID_OPERATOR, 0640) — maxx gets open EACCES; no setuid dmsetup/lvm. Root->kernel is game-over by definition so no privilege boundary to cross. No uid=0 chain possible. No exploit.c written because escalation is structurally impossible.
Evidence (decisive lines)
baseline (unpatched #0): Fatal trap 12 page fault, fault va=0x2e2e7a4a7054, current process=987, Stopped at _kfree+0x45: movl 0x54(%rax),%r13d, db>. patched dm.ko: message ioctl returned rv=22 (Invalid argument), RUN_EXIT=0, guest up, no panic in boot.log.
PoC changes
Authored PoC from scratch (dir empty). dm_uninit_msg.c uses libprop: (1) command=create name=df2446dev so dm_dev_lookup succeeds, then (2) command=message with 'message' key omitted so prop_dictionary_get_cstring leaves msg uninitialized and kfree(msg) frees stack garbage. fix.diff initializes msg=NULL, checks get_cstring return, guards kfree with msg!=NULL.
Verified recommended fix
In sys/dev/disk/dm/dm_ioctl.c dm_message_ioctl: (1) initialize char *msg = NULL; (2) check prop_dictionary_get_cstring return and return EINVAL on failure before kfree; (3) defensively guard if (msg != NULL) kfree(msg, M_TEMP). Full git-apply-able diff in findings/poc/DF-2446/fix.diff (git apply --check passes).
Verdict
REPRODUCED. dm_message_ioctl (sys/dev/disk/dm/dm_ioctl.c:998) declares char *msg; uninitialized, calls prop_dictionary_get_cstring(dm_dict, DM_MESSAGE_STR, &msg) at line 1028 without checking the return, then calls kfree(msg, M_TEMP) at line 1058. proplib leaves *cpp UNWRITTEN when the key is missing or wrong-typed, so msg keeps stack residue. Confirmed on the unpatched #0 kernel: a NETBSD_DM_IOCTL with command="message" and the "message" key omitted panics deterministically — Fatal trap 12: page fault, fault virtual address = 0x2e2e7a4a7054 (stack-garbage msg), Stopped at _kfree+0x45: movl 0x54(%rax),%r13d, guest wedged in DDB. Free-of-uninitialized-pointer primitive: deterministic local DoS, and a heap-corruption/UAF primitive if stack residue is groomed onto a valid slab address.
No comments yet.