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)
PoC verification
Evidence pack
findings/poc/DF-2229 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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); withpa=NULLthis ismtx_lock(&pa->pa_rwlock)at offset0x40from 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(needsversion=[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/controlis created when thedmmodule is loaded and is0640 root:operator(confirmed in-guest). So a local user in theoperatorgroup (and root trivially) can issue the ioctl and panic the kernel. Thedmmodule 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_ioctlparses it), so omittingcmd_datais 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 NOcmd_data, sendsNETBSD_DM_IOCTL(0xc010fd00) to/dev/mapper/controlviaprop_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
fixedVALIDATED: ./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.
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.
No comments yet.