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

prop_array_iterator and 3 siblings lack prop_object_is_array NULL/type guard enabling kernel panic via DM ioctl

Summary

Four public functions in prop_array.c: prop_array_iterator (538) prop_array_make_immutable (553) prop_array_mutable (567) prop_array_externalize (743) dereference prop_array_t argument without first calling prop_object_is_array(pa) unlike ALL seven other public accessors in same file (prop_array_capacity/count/ensure_capacity/get/set/add/remove/equals all guard). Passing NULL triggers kernel NULL deref panic. Confirmed reachable: dm_ioctl.c:707-708 passes unchecked return of prop_dictionary_get(dm_dict "cmd_data") directly to prop_array_iterator and dictionary parsed from attacker-controlled plist XML via prop_dictionary_copyin_ioctl. dm_check_version passes if version array [4 <=16 *] present. dm_table_load_ioctl at dm_ioctl.c:707 returns NULL when attacker omits cmd_data key line 708 prop_array_iterator(NULL) panic. DM control device /dev/mapper/control created 0640 root:operator. Attacker: local user in operator group with dm module loaded sends single NETBSD_DM_IOCTL with valid command:reload version array but omitting cmd_data key. Impact: immediate kernel panic pure DoS. Module not loaded by default GENERIC but any system using LVM2/device-mapper affected.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2229 Β· 9 files
FileTypeDescriptionSize
df2229.c trigger-source dm reload ioctl without cmd_data -> prop_array_iterator(NULL) 3.1 KB view raw
build.sh build-script cc -lprop 95 B view raw
run.sh run-script kldload dm; ./df2229 658 B view raw
run.log run-log baseline panic x2 + fixed-kernel no-panic 1.7 KB view raw
panic.txt panic-signature Fatal trap 12 at prop_array_iterator+0x1b, fault 0x40 244 B view raw
fix_build.log build-log single-fix kernel build log (35354 lines) 5.6 MB ↓ download
env.txt environment uname + kern.version (#0 baseline / #1 fixed) 209 B view raw
fix.diff suggested-fix add prop_object_is_array guard to 4 accessors 1019 B view raw
VERDICT.md verdict full narrative + fix validation 4.6 KB ↓ raw
VERDICT.md verdict full narrative + fix validation
↓ download raw

DF-2229 β€” prop_array_iterator and 3 siblings lack prop_object_is_array guard β†’ NULL deref panic

Verdict

REPRODUCED β€” local kernel NULL-deref panic; fix validated on a rebuilt kernel. Four public accessors in prop_array.c β€” prop_array_iterator, prop_array_make_immutable, prop_array_mutable, prop_array_externalize β€” dereference pa->pa_rwlock (or pa->pa_obj.po_type) with no prop_object_is_array(pa) type guard, unlike every other public accessor in the file (prop_array_capacity/count/ensure_capacity/get/set/add/remove/equals all guard). Passing NULL (or a non-array object) derefs near-NULL β†’ panic. The reachable trigger is the device-mapper reload ioctl: dm_table_load_ioctl (dm_ioctl.c:707-708) feeds the unchecked return of prop_dictionary_get(dm_dict, "cmd_data") straight into prop_array_iterator; omitting cmd_data from the attacker-controlled plist yields NULL β†’ panic, before any device-lookup check.

Mechanism (trigger β†’ primitive β†’ effect)

// sys/libprop/prop_array.c:538-545  (prop_array_iterator)
prop_object_iterator_t
prop_array_iterator(prop_array_t pa)
{
        prop_object_iterator_t pi;
        _PROP_RWLOCK_RDLOCK(pa->pa_rwlock);   // NO guard; pa=NULL -> mtx_lock
        ...
}
  • Kernel rwlock macro _PROP_RWLOCK_RDLOCK(x) = mtx_lock(&(x)) (prop_object_impl.h:297); with pa=NULL this is mtx_lock(&pa->pa_rwlock) at offset 0x40 from NULL β†’ write page-fault β†’ panic.
  • Trigger path: dmioctl (device-mapper.c:241) β†’ NETBSD_DM_IOCTL β†’ prop_dictionary_copyin_ioctl (parses attacker plist) β†’ dm_check_version (needs version=[4,<=16,*]) β†’ dm_cmd_to_fun ("reload") β†’ dm_table_load_ioctl (dm_ioctl.c:673): c cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA); // NULL if absent iter = prop_array_iterator(cmd_array); // line 708 -> panic // (device-lookup check is at line 711, AFTER this deref)

Evidence (panic, unpatched #0)

Fatal trap 12: page fault while in kernel mode
cpuid = 4
fault virtual address  = 0x40
fault code             = supervisor write data, page not present
Stopped at      prop_array_iterator+0x1b:   lock cmpxchgl %edx,0x40(%rdi)
db>

Reproduced on 2 independent fresh vm.sh reset with-src runs (deterministic). 0x40 is exactly the pa_rwlock offset from the NULL pa; the faulting cmpxchgl is the mtx_lock inside _PROP_RWLOCK_RDLOCK. See panic.txt.

Threat model / privilege boundary

  • /dev/mapper/control is created when the dm module is loaded and is 0640 root:operator (confirmed in-guest). So a local user in the operator group (and root trivially) can issue the ioctl and panic the kernel. The dm module is not loaded by default GENERIC but is standard on any system using LVM2/device-mapper. Impact: immediate local kernel panic / DoS. The plist is attacker-controlled (prop_dictionary_copyin_ioctl parses it), so omitting cmd_data is trivial.
  • Severity Medium / confidence certain (deterministic panic, reachable from an operator-group user once dm is available).

PoC changes

  • df2229.c (new): builds {command="reload", version=[4,16,0], name=..., flags=0} with NO cmd_data, sends NETBSD_DM_IOCTL (0xc010fd00) to /dev/mapper/control via prop_dictionary_send_ioctl.
  • build.sh (cc -lprop) / run.sh (kldload dm; ./df2229).

Fix (fix.diff)

Add the missing prop_object_is_array(pa) guard at the top of all four unguarded accessors, matching the pattern used by the seven already-guarded ones in the same file: - prop_array_iterator: if (!prop_object_is_array(pa)) return (NULL); - prop_array_make_immutable: if (!prop_object_is_array(pa)) return; - prop_array_mutable: if (!prop_object_is_array(pa)) return (false); - prop_array_externalize: if (!prop_object_is_array(pa)) return (NULL); Matches the finding markdown's proposal (add the type guard before the rwlock/deref).

Fix validation (rebuilt kernel)

prop_array.c is part of the in-kernel libprop, so the validation rebuilds the kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC): - Baseline (#0): ./df2229 β†’ Fatal trap 12: page fault at prop_array_iterator+0x1b (NULL deref, addr 0x40) (2/2 fresh-reset runs). - Fixed (#1, sha256 9fb1b67f..., fix.diff applied): ./df2229 β†’ prop_dictionary_send_ioctl returned 2 (ENOENT β€” the guarded prop_array_iterator(NULL) now returns NULL; dm_table_load_ioctl then proceeds to dm_dev_lookup which fails for the non-existent device and returns ENOENT), no panic, guest stays up (uptime healthy). fix_status = fixed.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: ./df2229 deterministically panics ('Fatal trap 12 page fault at prop_array_iterator+0x1b', addr 0x40) on unpatched #0 kernel (2/2 fresh-reset baseline runs) and does NOT panic on single-fix #1 kernel (rebuilt with fix.diff) β€” prop_dictionary_send_ioctl returns 2 (ENOENT: guarded prop_array_iterator(NULL) now returns NULL; dm_table_load_ioctl proceeds to dm_dev_lookup which fails for non-existent device), guest stays up => fix closes NULL deref.

baseline (#0): Fatal trap 12 page fault, addr=0x40, prop_array_iterator+0x1b: lock cmpxchgl %edx,0x40(%rdi) (2/2 runs). fixed (#1, 9fb1b67f...): 'df2229: prop_dictionary_send_ioctl returned 2' (ENOENT), PATCHED_RC=0, uptime '11:16AM up 25 secs' healthy, NO panic.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Aug 9 11:10:51 UTC 2026 (single-fix kernel, sha256 9fb1b67f9f9865f48a811b181e7dee33c91527c04cb1d510e3d5da44f7333913)

Confirmed kernel references

Detail

Exploit chain

none (NULL-deref panic / local DoS, not write primitive). /dev/mapper/control is 0640 root:operator (confirmed in-guest), so local operator-group user (root trivially) can issue ioctl once dm module loaded. Attacker plist omits cmd_data -> prop_array_iterator(NULL) -> immediate kernel panic. No escalation; pure DoS.

Evidence (decisive lines)

Baseline (#0): '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)' (2/2 fresh-reset runs). Fixed (#1, sha256 9fb1b67f...): 'prop_dictionary_send_ioctl returned 2' (ENOENT β€” guarded prop_array_iterator returns NULL, dm_dev_lookup fails gracefully), no panic, uptime healthy.

PoC changes

Created df2229.c (builds prop_dictionary {command='reload', version=[4,16,0], name, flags} WITHOUT cmd_data; sends NETBSD_DM_IOCTL _IOWR(0xfd,0,struct plistref)=0xc010fd00 via prop_dictionary_send_ioctl), build.sh, run.sh, fix.diff. Fixed ioctl number (real struct plistref is 16 bytes {void*;size_t}, not 8).

Verified recommended fix

Add 'if (!prop_object_is_array(pa)) return ...;' at top of all four unguarded accessors (prop_array_iterator->NULL, prop_array_make_immutable->return, prop_array_mutable->false, prop_array_externalize->NULL), matching pattern of seven already-guarded accessors in same file. Matches finding markdown proposal exactly. Full git-apply-able diff in findings/poc/DF-2229/fix.diff.

Verdict

REPRODUCED β€” local kernel NULL-deref panic; fix validated on rebuilt kernel. Four public accessors in prop_array.c β€” prop_array_iterator (538), prop_array_make_immutable (553), prop_array_mutable (567), prop_array_externalize (743) β€” dereference pa->pa_rwlock (or pa->pa_obj.po_type) with NO prop_object_is_array(pa) guard, unlike all 7 other public accessors in file. Passing NULL derefs near-NULL: kernel rwlock macro _PROP_RWLOCK_RDLOCK(x)=mtx_lock(&(x)) (prop_object_impl.h:297), so prop_array_iterator(NULL) -> mtx_lock at offset 0x40 from NULL -> write page-fault. Reachable trigger: dm_table_load_ioctl (dm_ioctl.c:707-708) feeds unchecked prop_dictionary_get(dm_dict,'cmd_data') (NULL when key omitted) straight into prop_array_iterator BEFORE device-lookup check. Confirmed by deterministic panic on 2 fresh-reset runs; fixed kernel returns ENOENT cleanly.