dm_table_load_ioctl: NULL-pointer-dereference panic via missing cmd_data array
Summary
dm_table_load_ioctl fetches DM_IOCTL_CMD_DATA with prop_dictionary_get (returns NULL when key absent/not array) and immediately passes result to prop_array_iterator(). In kernel proplib prop_array_iterator expands to _PROP_RWLOCK_RDLOCK(pa->pa_rwlock)==mtx_lock(&(pa->pa_rwlock)) BEFORE any NULL check. With pa==NULL mtx_lock on near-zero address unrecoverable page fault kernel mode. prop_object_is_array guard only inside inner _prop_array_iterator_locked reached AFTER lock acquisition cannot save caller. Attacker: operator group send reload command lacking cmd_data key. 100% reliable kernel panic no race no grooming.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2448 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dm_nulldata_deref.c | trigger-source | libprop NETBSD_DM_IOCTL: create + reload w/o cmd_data | 4.7 KB | view raw |
| build.sh | build-script | cc -o dm_nulldata_deref dm_nulldata_deref.c -lprop | 86 B | view raw |
| run.sh | run-script | kldload dm + ./dm_nulldata_deref (as root) | 247 B | view raw |
| build.log | build-log | PoC compile output (rc=0) | 130 B | view raw |
| run.log | run-log | baseline unpatched run: stdout + panic signature | 875 B | view raw |
| fix_run.log | run-log | patched-module run: EINVAL (22), no panic | 570 B | view raw |
| fix_build.log | build-log | single-fix dm.ko module build output (rc=0) | 45.1 KB | view raw |
| fix.diff | suggested-fix | NULL check on cmd_array + iter in dm_table_load_ioctl | 622 B | view raw |
| panic.txt | panic-signature | Fatal trap 12 / prop_array_iterator+0x1b / fault VA 0x40 | 1.5 KB | view raw |
| env.txt | environment | uname, cc, kern.version, dm.ko info, /dev/mapper/control perms | 590 B | view raw |
| README.md | readme | claim + reproduce + expected + privilege | 1.8 KB | β raw |
| VERDICT.md | verdict | full mechanism walkthrough + Phase 8 fix validation | 6.4 KB | β raw |
DF-2448 β dm_table_load_ioctl NULL-deref via missing DM_IOCTL_CMD_DATA
Claim
dm_table_load_ioctl (sys/dev/disk/dm/dm_ioctl.c:673) fetches
DM_IOCTL_CMD_DATA with prop_dictionary_get (returns NULL when key absent)
and immediately passes the result to prop_array_iterator(). In kernel
proplib, prop_array_iterator does _PROP_RWLOCK_RDLOCK(pa->pa_rwlock) ==
mtx_lock(&(pa->pa_rwlock)) BEFORE any NULL check β NULL-deref panic
when the cmd_data key is missing/not-an-array.
Reproduce
./build.sh # cc -o dm_nulldata_deref dm_nulldata_deref.c -lprop
ssh dfbsd 'kldload dm' # root only; creates /dev/mapper/control (0640 root:operator)
ssh dfbsd 'cd poc/DF-2448 && ./dm_nulldata_deref'
Expected (bug present, unpatched dm.ko)
Kernel panic, guest drops to DDB, ssh dies:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x40 Stopped at prop_array_iterator+0x1b: lock cmpxchgl %edx,0x40(%rdi) db>
Expected (after fix.diff applied, patched dm.ko)
No panic; PoC prints:
[!] reload ioctl returned rv=22 (Invalid argument) -- kernel survived?
Guest stays up; can be re-run any number of times.
Privilege
The dm module must be kldload-ed by root, and /dev/mapper/control is
0640 root:operator. So this bug is reachable only by root or
operator-group members. It is a root/operator β kernel DoS /
hardening gap, NOT an unprivileged β root escalation. The primitive is a
NULL-deref page fault (no write), so there is no escalation chain.
Files
dm_nulldata_deref.cβ PoC source (libprop NETBSD_DM_IOCTL).build.sh/run.shβ build/run scripts.fix.diffβ verified fix (NULL check on cmd_array + iter).VERDICT.mdβ full mechanism walkthrough + Phase 8 validation.
DF-2448 β dm_table_load_ioctl NULL-deref via missing DM_IOCTL_CMD_DATA
Verdict
REPRODUCED (panic / local DoS) + FIX VALIDATED. The bug is real and
deterministically crashes the kernel the moment the inbound dictionary OMITS
the "cmd_data" key. The escalation to uid=0 is blocked by a valid hard
blocker: this is a pure NULL-deref read-fault-equivalent (a mtx_lock
on &NULL->pa_rwlock β a kernel write to virt addr 0x40, taken before any
attacker-controlled content is read or written) and yields no write
primitive, so there is no corruption to groom and no escalation chain.
Realistic impact ceiling: local DoS (root/operator can panic the kernel).
The authored fix.diff is built as a single-fix dm.ko module, installed,
and confirmed to close the bug (panic β clean EINVAL).
Mechanism (trigger β primitive β effect)
dm_table_load_ioctl() in sys/dev/disk/dm/dm_ioctl.c fetches the
"cmd_data" array and passes it straight to prop_array_iterator() with no
NULL check:
673: int 674: dm_table_load_ioctl(prop_dictionary_t dm_dict) ... 707: cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA); 708: iter = prop_array_iterator(cmd_array); <-- NULL deref
prop_dictionary_get()(sys/libprop/prop_dictionary.c:933) returnsNULLwhen the key is absent (_prop_dictionary_getat line 909:pdestays NULL βpostays NULL β returns NULL).prop_array_iterator()(sys/libprop/prop_array.c:538) does:
538: prop_array_iterator(prop_array_t pa)
539: {
540: prop_object_iterator_t pi;
541:
542: _PROP_RWLOCK_RDLOCK(pa->pa_rwlock); <-- FIRST statement
543: pi = _prop_array_iterator_locked(pa); <-- only HERE does it
544: _PROP_RWLOCK_UNLOCK(pa->pa_rwlock); check is_array(pa)
545: return (pi);
546: }
Under _KERNEL, _PROP_RWLOCK_RDLOCK(pa->pa_rwlock) expands
(sys/libprop/prop_object_impl.h:297) to:
mtx_lock(&(pa->pa_rwlock))
i.e. mtx_lock(&(((struct prop_array *)NULL)->pa_rwlock)). With pa == NULL
that is a kernel write to virt address offsetof(struct prop_array, pa_rwlock)
= 0x40 β and crucially it happens before the
prop_object_is_array(pa) guard inside _prop_array_iterator_locked()
(line 517) can run, so the NULL is never caught. Result: page fault in
kernel mode β panic.
Observed crash signature (boot.log)
Fatal user address access from kernel mode from dm_nulldata_dere at ffffffff809d9a7b Fatal trap 12: page fault while in kernel mode fault virtual address = 0x40 fault code = supervisor write data, page not present Stopped at prop_array_iterator+0x1b: lock cmpxchgl %edx,0x40(%rdi) db>
The fault VA 0x40 and the stopped-at instruction cmpxchgl %edx,0x40(%rdi)
with %rdi==0 exactly match the macro expansion mtx_lock(&(pa->pa_rwlock))
with pa==NULL.
Trigger
A NETBSD_DM_IOCTL (sys/dev/disk/dm/netbsd-dm.h:41,
_IOWR(DM_IOCTL, 0, struct plistref)) carrying a libprop dictionary with:
"version"=[4, 0, 0]β passesdm_check_version()(major==4, minor<=16),"command"="reload"β routes throughdm_cmd_to_fun()indevice-mapper.c:286todm_table_load_ioctl(cmd_fn table line 131),"name"= any string,"cmd_data"OMITTED β the trigger.
The NULL deref at line 708 fires before the dm_dev_lookup at line 711,
so no device needs to exist; the PoC creates one (command="create") only to
mirror a realistic scenario (legitimate device + malformed reload).
Privilege analysis β why this is DoS, not privesc
- Module load: the
dmdriver is a KLD module; reaching the ioctl requireskldload dm, which is a root-only operation (PRIV_KLD_LOAD). - Device node:
/dev/mapper/controlis created asmake_dev(&dmctl_ops, 0, UID_ROOT, GID_OPERATOR, 0640, "mapper/control")(device-mapper.c:181) βcrw-r----- root operator. Themaxxuser (uid 1001, not inoperator/wheel) getsEACCESonopen(). - So the bug is reachable only by root or an
operator-group member. Rootβkernel is game-over by definition; anoperator-group member panicking the kernel is a hardening/robustness gap.
Combined with the fact that the primitive is a read-fault-equivalent NULL deref (no attacker-controlled bytes are read or written before the trap), there is no escalation chain to develop β this is the valid hard blocker for Phase 6.
Exploit chain
none β pure NULL-deref panic, no write primitive (valid hard blocker).
No escalation file (exploit.c) is produced because there is no corruption
to convert.
Fix (fix.diff)
Minimal, root-cause-targeted: check the prop_dictionary_get return for NULL
before calling prop_array_iterator, returning EINVAL if "cmd_data" is
missing/non-array; and also check the iterator return for NULL (ENOMEM):
cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA);
if (cmd_array == NULL) {
dmdebug("%s: DM_IOCTL_CMD_DATA missing\n", __func__);
return EINVAL;
}
iter = prop_array_iterator(cmd_array);
if (iter == NULL) {
dmdebug("%s: prop_array_iterator failed\n", __func__);
return ENOMEM;
}
(There is no finding markdown for DF-2448 yet β findings/DF-2448-*.md does
not exist in this tree. The fix.diff is the authoritative verified fix.)
Fix validation (Phase 8)
- Baseline (
with-srcsnapshot, kernel6.5-DEVELOPMENT #0, unpatcheddm.ko): PoC panics deterministically βFatal trap 12,Stopped at prop_array_iterator+0x1b. Guest DDB, must reset. - Patched: applied
fix.diffto/usr/src/sys/dev/disk/dm/dm_ioctl.c, built the dm module alone (makeinsys/dev/disk/dmβ module build, ~30 s, no full kernel rebuild needed), installeddm.koβ/boot/kernel/dm.ko,kldload dm. - Re-run: same PoC returns
EINVAL(errno 22) βreload ioctl returned rv=22 (Invalid argument). Guest stays up. Repeated 3Γ β deterministic. - Verdict: fix closes the bug (panic β clean EINVAL).
PoC
dm_nulldata_deref.c β libprop NETBSD_DM_IOCTL:
1. command="create", name="df2448dev" (cosmetic; the deref is upstream
of the device lookup).
2. command="reload", name="df2448dev", "cmd_data" omitted β
prop_dictionary_get returns NULL β prop_array_iterator(NULL) β panic.
Build: cc -o dm_nulldata_deref dm_nulldata_deref.c -lprop
Run (as root, after kldload dm): ./dm_nulldata_deref
Fix verification
fixedVALIDATED: dm_nulldata_deref panics on unpatched dm.ko (Fatal trap 12, Stopped at prop_array_iterator+0x1b, fault VA 0x40 β guest wedged) and does NOT panic on single-fix dm.ko (reload ioctl returns rv=22 EINVAL, guest stays up, reproducible 3x). Module-only build (make in sys/dev/disk/dm, ~30s, rc=0) suffices because dm is KLD module. Fix closes the bug.
baseline (unpatched dm.ko): Fatal trap 12 page fault / fault virtual address = 0x40 / Stopped at prop_array_iterator+0x1b: lock cmpxchgl %edx,0x40(%rdi) / db> -> guest DOWN. patched (dm.ko rebuilt from fix.diff, sha256 dc0a27df...): reload ioctl returned rv=22 (EINVAL), RUN_EXIT=0 -> guest UP (x3 deterministic).
Confirmed kernel references
Detail
Exploit chain
none β valid hard blocker. Primitive is NULL-deref page fault on kernel-mode write to virt addr 0x40 (mtx_lock on &NULL->pa_rwlock). No attacker-controlled bytes read or written before the trap, so no corruption primitive to groom, no write to convert, no escalation chain. Additional reachability constraint: dm is KLD module (kldload dm root-only via PRIV_KLD_LOAD) and /dev/mapper/control created 0640 root:operator (device-mapper.c:181), so reachable only by root or operator-group β maxx (uid 1001) gets EACCES. Realistic impact ceiling: local DoS (root/operator can panic kernel). No exploit.c/chain.c produced because no corruption to escalate.
Evidence (decisive lines)
baseline (unpatched 6.5-DEVELOPMENT #0): [*] create 'df2448dev': rv=0 (ok) / [*] sending 'reload' ioctl WITHOUT 'cmd_data' key... / Fatal user address access from kernel mode from dm_nulldata_dere at ffffffff809d9a7b / Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x40 / fault code = supervisor write data, page not present / Stopped at prop_array_iterator+0x1b: lock cmpxchgl %edx,0x40(%rdi) / db> (guest DOWN, must reset).
PoC changes
Authored entire PoC from scratch (dir did not exist; no finding markdown exists either). dm_nulldata_deref.c: libprop NETBSD_DM_IOCTL that (1) issues command='create' to make a dm device (cosmetic β deref is upstream of device lookup at dm_ioctl.c:711), then (2) issues command='reload' with DM_IOCTL_CMD_DATA deliberately OMITTED -> prop_dictionary_get returns NULL -> prop_array_iterator(NULL) -> panic. Modeled on DF-2446 sibling. Build/run scripts and full evidence pack saved.
Verified recommended fix
fix.diff adds NULL guard in dm_table_load_ioctl right after prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA): if cmd_array is NULL return EINVAL (covers missing OR non-array key); also check prop_array_iterator return for NULL and return ENOMEM. Root-cause fix, closes bug deterministically (panic -> clean EINVAL, validated 3x). No finding markdown exists, so this fix.diff is authoritative verified proposal.
Verdict
REPRODUCED (panic / local DoS) + FIX VALIDATED. The bug is real and deterministic: dm_table_load_ioctl (sys/dev/disk/dm/dm_ioctl.c:707-708) calls prop_array_iterator(cmd_array) on the unchecked return of prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA); when 'cmd_data' is omitted from the libprop dictionary, prop_dictionary_get returns NULL (prop_dictionary.c:933), and prop_array_iterator(NULL) (prop_array.c:538) executes _PROP_RWLOCK_RDLOCK(pa->pa_rwlock) -> mtx_lock(&(NULL->pa_rwlock)) (prop_object_impl.h:297) BEFORE the prop_object_is_array() guard can run, faulting at virt addr 0x40. Confirmed on unpatched 6.5-DEVELOPMENT #0 kernel: 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0x40', 'Stopped at prop_array_iterator+0x1b: lock cmpxchgl %edx,0x40(%rdi)' (db> prompt, guest wedged).
No comments yet.