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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2448 Β· 12 files
FileTypeDescriptionSize
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
README.md readme claim + reproduce + expected + privilege
↓ download 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.
VERDICT.md verdict full mechanism walkthrough + Phase 8 fix validation
↓ download raw

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
 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] β€” passes dm_check_version() (major==4, minor<=16),
  • "command" = "reload" β€” routes through dm_cmd_to_fun() in device-mapper.c:286 to dm_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

  1. Module load: the dm driver is a KLD module; reaching the ioctl requires kldload dm, which is a root-only operation (PRIV_KLD_LOAD).
  2. Device node: /dev/mapper/control is created as make_dev(&dmctl_ops, 0, UID_ROOT, GID_OPERATOR, 0640, "mapper/control") (device-mapper.c:181) β€” crw-r----- root operator. The maxx user (uid 1001, not in operator/wheel) gets EACCES on open().
  3. So the bug is reachable only by root or an operator-group member. Root→kernel is game-over by definition; an operator-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)

  1. Baseline (with-src snapshot, kernel 6.5-DEVELOPMENT #0, unpatched dm.ko): PoC panics deterministically β€” Fatal trap 12, Stopped at prop_array_iterator+0x1b. Guest DDB, must reset.
  2. Patched: applied fix.diff to /usr/src/sys/dev/disk/dm/dm_ioctl.c, built the dm module alone (make in sys/dev/disk/dm β€” module build, ~30 s, no full kernel rebuild needed), installed dm.ko β†’ /boot/kernel/dm.ko, kldload dm.
  3. Re-run: same PoC returns EINVAL (errno 22) β€” reload ioctl returned rv=22 (Invalid argument). Guest stays up. Repeated 3Γ— β€” deterministic.
  4. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; only dm KLD module rebuilt from patched /usr/src/sys/dev/disk/dm and installed to /boot/kernel/dm.ko, sha256 dc0a27df895e96e152b5982c1fbbc3aac08ca77f5e0724d928d829123417e4b1)

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).