prop_array_set/prop_array_remove enforce index bounds only via INVARIANTS-gated assert: stock-kernel panic (reproduced live), silent heap OOB write + prop_object_release of an OOB-read pointer on non-INVARIANTS kernels
Summary
Both writers validate idx with _PROP_ASSERT only (KKASSERT - compiled to nothing without INVARIANTS): stock kernels panic on any out-of-range index; non-INVARIANTS production kernels read an 8-byte pointer OOB, write a kernel pointer OOB at an arbitrary 32-bit index, and call prop_object_release on the OOB-read value (refcount decrement at attacker-shaped address + pot_free dispatch on a fake object; remove additionally underflows count on empty arrays). prop_array_get shows the intended real-check contract the writers fail to mirror. No in-tree kernel caller forwards a plist-derived index today (all audited) - latent, one API misuse away in the kernel's untrusted-plist container library. Proven live: KLD triggers panic the stock guest at :648/:701; NDEBUG rig SIGSEGVs inside prop_object_release on the OOB-read garbage (wild-release primitive self-demonstrating); guard-page rig faults at exactly the predicted offset. real-check fix validated on a full kernel rebuild (both modules complete, 12/12 rig battery clean).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5678 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 7.2 KB | β raw | |
| df5678_userland.c | β | 5.3 KB | view raw | |
| guardalloc.h | β | 2.7 KB | view raw | |
| df5678_mod.c | β | 2.7 KB | view raw | |
| build.sh | β | 219 B | view raw | |
| run.sh | β | 360 B | view raw | |
| run.userland.log | β | 2.3 KB | view raw | |
| run.userland.fixed.log | β | 2.2 KB | view raw | |
| panic.set.txt | β | 776 B | view raw | |
| panic.remove.txt | β | 755 B | view raw | |
| fixtest.console.txt | β | 401 B | view raw | |
| build.baseline.log | β | 520 B | view raw | |
| build.fixed.log | β | 520 B | view raw | |
| build.tail.txt | β | 55 B | view raw | |
| env.txt | β | 678 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| verdict.json | β | 5.6 KB | view raw |
DF-5678 β prop_array_set/prop_array_remove bounds enforced only by INVARIANTS-gated assert
sys/libprop/prop_array.c β pass 2 (GLM 5.3 re-audit).
The defect
prop_array_set() and prop_array_remove() validate their index argument
only via _PROP_ASSERT:
sys/libprop/prop_array.c:648β_PROP_ASSERT(idx < pa->pa_count);(prop_array_set), immediately followed by OOB readopo = pa->pa_array[idx](:650), OOB writepa->pa_array[idx] = po(:654), andprop_object_release(opo)on the OOB-read value (:657).sys/libprop/prop_array.c:701β same assert (prop_array_remove), followed by OOB readpo = pa->pa_array[idx](:709) andprop_object_release(po)of the garbage pointer (:719).
In the kernel _PROP_ASSERT == KKASSERT (sys/libprop/prop_object_impl.h:258),
which is compiled in only when options INVARIANTS is set
(sys/sys/systm.h:94-122):
- stock X86_64_GENERIC (has INVARIANTS): kernel panic β reproduced live.
- any kernel built without INVARIANTS (a fully supported production
configuration): the check is compiled to nothing and both functions
perform a silent heap OOB write of a kernel pointer at an arbitrary
32-bit index plus
prop_object_release()of an out-of-bounds-read pointer (arbitrary-pointer refcount decrement / potential free).
Contrast prop_array_get() at :591, which does a real if (idx >= pa->pa_count)
bounds check β the intended contract the two writers fail to mirror.
No current in-tree kernel caller passes an attacker-derived index
(audited: dm_ioctl.c fixed indices 0..2 are provably safe because of the
idx == pa_count append design at :643; dm_ioctl.c:649 loops over
prop_array_count; vfs_quota.c/kern_udev.c use add/iterator only) β so the
kernel reachability today is latent, one API misuse away. proplib is the
kernel's untrusted-plist container library, which is why this is filed.
Reproduce (evidence classes: same-source userland differential + KLD)
On the audit guest (source snapshot, /usr/src/sys/libprop present):
scp df5678_userland.c guardalloc.h df5678_mod.c build.sh run.sh fix.diff dfbsd:/tmp/
vm.sh run_root 'mkdir -p /tmp/df5678 && mv /tmp/df5678_* /tmp/guardalloc.h /tmp/build.sh /tmp/run.sh /tmp/fix.diff /tmp/df5678/ 2>/dev/null; cd /tmp/df5678 && sh build.sh'
vm.sh run_root 'cd /tmp/df5678 && sh run.sh' # userland differential
vm.sh run_root 'kldload -v /tmp/df5678/kmod_set/df5678.ko' # PANIC at :648
# ... reset, then:
vm.sh run_root 'kldload -v /tmp/df5678/kmod_rem/df5678rem.ko' # PANIC at :701
Expected output (baseline)
df5678_assert setβAssertion failed: (idx < pa->pa_count), function prop_array_set, file prop_array.c, line 648exit 134 (stock-kernel analog).df5678_ndebug setβ SIGSEGV inprop_object_releaseon an OOB-read garbage pointer (wild release), exit 139 (non-INVARIANTS-kernel analog).df5678_guard setβ SIGSEGV withsi_addrin the guard page at exactlypa_array[512]; theremovefault address differs by exactly(600-512)*8 = 704bytes β the touched slots arepa_array[512]/[600].- KLD loads β
panic: assertion "idx < pa->pa_count" failed in prop_array_set/prop_array_remove at .../prop_array.c:648/:701(panic.set.txt / panic.remove.txt).
After fix.diff (validated)
- userland battery: every variant/scenario exit 0;
set(512)returns 0 (false);remove(600)no-ops; guard rig raises no fault (run.userland.fixed.log). - full
make -j8 nativekernel+installkernel+ reboot: both KLDs load,set(5) returned 0, count=2/remove(7) returned, count=2,module completed without panic(fixtest.console.txt).
Files
df5678_userland.cβ 3-variant differential harnessguardalloc.hβ rig-only guard-page allocator (patches the rig copy of prop_object_impl.h;sys/is never modified)df5678_mod.cβ kernel trigger modules (MODE_SET / MODE_REM)build.sh,run.shβ exact runnable commandsrun.userland.log,run.userland.fixed.logβ baseline / fixed batteriespanic.set.txt,panic.remove.txtβ stock-kernel panics (serial console)fixtest.console.txtβ fixed-kernel module runs, no panicbuild.baseline.log,build.fixed.logβ rig build logsenv.txtβ guest environment (fixed-kernel phase; baseline uname in VERDICT.md)fix.diffβ git-apply-able unified diff (validated)
DF-5678 VERDICT β reproduced (kernel panic proven live; silent OOB write + wild release proven on the same source with the assert compiled out)
File: sys/libprop/prop_array.c (pass 2, GLM 5.3)
Baseline guest kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (INVARIANTS on β verified:
strings /boot/kernel/kernel | grep -c 'assertion "%s" failed' == 3, and both panics below).
Fixed guest kernel: #1: Fri Oct 9 23:00:04 UTC 2026 (full make -j8 nativekernel
KERNCONF=X86_64_GENERIC + installkernel of fix.diff).
Root cause (path:line)
sys/libprop/prop_array.c:648β_PROP_ASSERT(idx < pa->pa_count);inprop_array_set()is the ONLY index bound before::650opo = pa->pa_array[idx];β OOB read (8-byte kernel pointer):654pa->pa_array[idx] = po;β OOB write (kernel pointer at arbitrary 32-bit index):657prop_object_release(opo);β release of the OOB-read pointer (refcount decrement atgarbage+offsetof(po_refcnt); on 0 βpot_freedispatch on a fake object)sys/libprop/prop_array.c:701β same inprop_array_remove(), followed by the OOB read at:709andprop_object_release(po)of garbage at:719; thepa->pa_count--at:714additionally underflows when count==0._PROP_ASSERTisKKASSERTin the kernel (sys/libprop/prop_object_impl.h:258) andKKASSERTis active only withoptions INVARIANTS(sys/sys/systm.h:94-122β the#elseat :116-118 defines it todo {} while(0)). StockX86_64_GENERIChas INVARIANTS (sys/config/X86_64_GENERIC:56); kernels built without it β a fully supported, common production configuration β compile the bound check away.- The intended contract is visible in the same file:
prop_array_get()does a real check at:591(if (idx >= pa->pa_count) goto out;).
What was reproduced, and how
1. Stock-kernel analog + live kernel proof (assert live)
- Same-source userland rig (assert build):
setaborts withAssertion failed: (idx < pa->pa_count), function prop_array_set, file prop_array.c, line 648(exit 134);removeaborts at line 701 (run.userland.log, section A). - Live kernel:
kldload df5678.ko(callsprop_array_set(pa,5,po)on a count=2 array inside MOD_LOAD) βpanic: assertion "idx < pa->pa_count" failed in prop_array_set at /usr/src/sys/libprop/prop_array.c:648with the full backtrace throughdf5678_modevent(panic.set.txt). After reset,kldload df5678rem.ko(prop_array_remove(pa,7)) β panic at prop_array.c:701 (panic.remove.txt). This proves the defective code is compiled into and executes in the running stock kernel.
2. Non-INVARIANTS-kernel analog (assert compiled out)
- NDEBUG rig (plain libc malloc):
set(512)on a count=capacity=510 array β SIGSEGVsi_addr=0x89736c6162β the fault is insideprop_object_release(opo)at :657 dereferencing the garbage pointer read out of bounds at :650: the wild-release primitive demonstrating itself.remove(600)β SIGSEGVsi_addr=0x0(release of an OOB-read NULL-ish value). (run.userland.log, section B; the handler's fixed text says "guard page" β in section B the faults are wild-pointer derefs, in section C they are true guard-page hits.) - Guard rig (NDEBUG + guard-page allocator around every
_PROP_*allocation; array backing store of 5108+16 == 4096 bytes ends flush with aPROT_NONEpage):set(512)faults atsi_addr=0x8004f3010,remove(600)atsi_addr=0x8004f32d0. The addresses differ by exactly 0x2c0 = 704 = (600-512)8** β i.e. the faulting accesses are precisely atpa_array[512]andpa_array[600], the slots the source analysis predicts (:650/:654 and :709). No sanitizer needed.
3. Reachability (honest scope)
No in-tree kernel caller passes an attacker-derived index today:
sys/dev/disk/dm/dm_ioctl.c:1090-1092βprop_array_set_uint32(ver, 0/1/2, ...): fixed indices 0..2 are provably safe becauseprop_array_set'sidx == pa_countbranch (:643) appends, and the three sequential calls raise count to β₯2 before index 2 is used as a replace.sys/dev/disk/dm/dm_ioctl.c:649β get loop bounded byprop_array_count(array);prop_array_getreal-checks anyway (:591).sys/kern/vfs_quota.c,sys/kern/kern_udev.c, dm_dev/dm_target β only add/iterator/copyout usage.
So the kernel impact today is latent: one future (or currently unknown) caller that forwards a plist-derived index β the exact use pattern this container library exists for β turns this into a stock-kernel panic (INVARIANTS) or, on non-INVARIANTS kernels, an attacker-shaped heap OOB write plus arbitrary-pointer release (refcount decrement β UAF). The primitive is the reason this is Medium and not Low.
4. Fix validation (full kernel rebuild)
fix.diffadds real bounds checks mirroringprop_array_get:idx > pa->pa_count β goto out(rv=false) inprop_array_set, andidx >= pa_count β unlock+returninprop_array_remove. git-apply and patch(1) clean against the read-only tree; applied only inside the guest's/usr/srccopy.- Patched-source userland rig: all variants/scenarios exit 0;
set(512)returns 0;remove(600)no-ops; guard rig raises no fault (run.userland.fixed.log). Controls and the legalidx == countappend path are unchanged. make -j8 nativekernel KERNCONF=X86_64_GENERIC+installkernel+ reboot into kernel #1 (23:00:04): both trigger modules load and printset(5) returned 0, count=2/remove(7) returned, count=2/module completed without panic(fixtest.console.txt). Baseline panics: panic.set.txt / panic.remove.txt. Bad behavior gone in both builds.- The kernel-build log was lost to the guest's /tmp-cleaning reboot; the
kernel identity (
#1: Fri Oct 9 23:00:04vs baseline#0: Thu Jul 2), env.txt, and the before/after behavioral evidence carry the validation.
Differential-harness notes
- The segv_handler message text ("guard page after prop_array backing store") is generic for all three binaries; it is literally true for the guard rig (C) and means "wild pointer deref" for the NDEBUG rig (B).
df5678_ndebug setfaulting instead of "surviving silently" is a stronger result than the README predicted: the wild release at :657/:719 fires immediately in this heap layout; on other layouts the corruption is silent until later (that silence is exactly the non-INVARIANTS production risk).
Verdict
reproduced β both halves of the differential (assert-live panic at prop_array.c:648/:701 in the running stock kernel; assert-compiled-out OOB read/write + release-of-garbage with slot-exact fault localization on the same source), and fixed on a full kernel rebuild (panics gone, bounds enforced, legal paths unchanged).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied inside guest /usr/src, full make -j8 nativekernel KERNCONF=X86_64_GENERIC + installkernel + reboot into #1 (Fri Oct 9 23:00:04): baseline panics at prop_array.c:648/:701 are gone β both trigger modules load and complete without panic (set(5) returns 0/false, remove(7) no-ops). Patched-source userland rig: all 12 variant/scenario cells exit 0, no asserts, no guard faults, legal in-bounds and idx==count append paths unchanged.
fixtest.console.txt (both modules 'completed without panic' on fixed kernel); run.userland.fixed.log (clean battery); panic.set.txt/panic.remove.txt (baseline); env.txt (fixed-kernel uname, kldstat)
Confirmed kernel references
- sys/libprop/prop_array.c:648
- sys/libprop/prop_array.c:650
- sys/libprop/prop_array.c:654
- sys/libprop/prop_array.c:657
- sys/libprop/prop_array.c:701
- sys/libprop/prop_array.c:709
- sys/libprop/prop_array.c:714
- sys/libprop/prop_array.c:719
- sys/libprop/prop_array.c:591
- sys/libprop/prop_object_impl.h:258
- sys/sys/systm.h:94
- sys/config/X86_64_GENERIC:56
Detail
Evidence (decisive lines)
panic.set.txt + panic.remove.txt (stock-kernel KKASSERT panics at prop_array.c:648/:701 with backtraces through df5678_modevent); run.userland.log (assert rig aborts naming :648/:701; NDEBUG rig SIGSEGVs in prop_object_release on OOB-read garbage si_addr=0x89736c6162; guard rig si_addr delta exactly 704 bytes == (600-512)*8 proving slot-exact OOB); run.userland.fixed.log (all variants exit 0, set returns 0, remove no-ops); fixtest.console.txt (fixed kernel #1: both modules load, 'module completed without panic'); fix.diff
PoC changes
Harnesses written fresh for this file: 3-variant same-source userland differential (assert/NDEBUG/guard β the guard variant needed a rig-only guarded allocator since the guest base gcc has no usable ASAN, following DF-5676's guard-page pattern), plus two KLD trigger modules. Repairs along the way: MOD_QUIET does not exist on DragonFly (compile error); bsd.kmod.mk drops CFLAGS+= so module modes must be baked into per-variant source copies (first rem-module silently ran SET code); unused-variable -Werror in the REM variant; /tmp is wiped on guest reboots (re-push needed each boot).
Verified recommended fix
Replace the two _PROP_ASSERT bounds in prop_array_set/prop_array_remove with real checks mirroring prop_array_get (see fix.diff).
Verdict
prop_array_set()/prop_array_remove() enforce the idx<pa_count bound only via _PROP_ASSERT==KKASSERT, which is INVARIANTS-gated (sys/sys/systm.h:94-122): the running stock X86_64_GENERIC kernel panics at prop_array.c:648/:701 (both reproduced live via KLD trigger modules, full backtraces in panic.set.txt/panic.remove.txt), while the same source with the assert compiled out (-DNDEBUG analog of a supported non-INVARIANTS kernel build) performs the OOB read at :650/:709, the OOB write at :654, and prop_object_release() of the OOB-read garbage at :657/:719 β the wild release fired on its own in the NDEBUG rig (SIGSEGV on a heap-garbage pointer), and the guard rig localized the faults to exactly pa_array[512]/pa_array[600] (delta 704 == (600-512)*8). No in-tree kernel caller currently forwards an attacker-derived index (dm's fixed 0..2 set-indices are provably safe via the idx==count append at :643; other callers use count-bounded get/add/iterator), so kernel reachability is latent β one plist-index-forwarding caller away from stock-kernel DoS or, on non-INVARIANTS kernels, attacker-shaped heap corruption. Fixed and validated on a full nativekernel rebuild: panics gone, set() returns false, remove() no-ops, all legal paths unchanged.
No comments yet.