_PROP_ONCE_RUN sets completion flag before calling init racing concurrent first-user into NULL-deref on uninitialized rb_tree
Summary
Kernel-mode _PROP_ONCE_RUN macro (prop_object_impl.h:303) atomically sets once-flag to 1 BEFORE calling init function f(). Concurrent thread losing cmpset sees flag==1 proceeds past guard without waiting for _prop_dict_init() to complete. Losing caller enters _prop_dict_keysym_alloc or _prop_dictionary_lock operating on global _prop_dict_keysym_tree (rbt_ops still NULL) and global mutex (struct lock not yet lockinitd) causing NULL pointer dereference panic in _prop_rb_tree_find. _prop_rb_tree_find (prop_rb.c:92-93) unconditionally dereferences rbto (NULL) to load compare function pointer panicking at small near-zero address. Userspace build uses pthread_once (correct) only kernel build has this bug. In practice first proplib call occurs during single-threaded boot closing window. Race exploitable when first use deferred to multi-threaded post-boot e.g. lazily-loaded kernel module with first two ioctl invocations on different CPUs simultaneously. Impact kernel panic DoS no privilege escalation or info leak. [VERIFIED pass2: window-state trigger Fatal trap 12 VA 0x8 at _prop_rb_tree_find+0x18; in-situ 6-thread race harness-limited, single-threaded boot closes the wild window; two-phase once fix-validated survivor waits for completer]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2225 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| run.log | β | 819 B | view raw | |
| run_stock_m1_boot.log | β | 13.7 KB | view raw | |
| panic.txt | β | 470 B | view raw | |
| run_stock_m0_boot.log | β | 17.4 KB | view raw | |
| run_fixed_m1_boot.log | β | 13.1 KB | view raw | |
| fix.diff | β | 616 B | view raw | |
| dfh.c | β | 24.3 KB | view raw | |
| dfarm.c | β | 2.4 KB | view raw | |
| harness.diff | β | 2.2 KB | view raw | |
| build.sh | β | 139 B | view raw | |
| run.sh | β | 145 B | view raw | |
| env.txt | β | 234 B | view raw |
Fix verification
fixedStock: Fatal trap 12 VA 0x8 at _prop_rb_tree_find+0x18 on the window-arm trigger. Fixed kernel, same trigger: the loser waits ~250 ms for the delayed completer, once transitions 1->2, prop_dictionary_set completes, guest healthy; boot-state check prints once=2 (stock prints 1).
run_stock_m1_boot.log (panic) vs run_fixed_m1_boot.log (delayed init completed (once=2), shell continues); B1 boot control shows once=2 at first use.
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt + run_stock_m1_boot.log: Stopped at _prop_rb_tree_find+0x18 movq 0x8(%r15),%rbx with fault VA 0x8 after the window-arm printout. run_stock_m0_boot.log: once==1 right after boot (window closed in practice). run_fixed_m1_boot.log: delayed init completed (once=2) and the caller survived and returned on the fixed kernel.
PoC changes
Original sketch had no runnable PoC. Built: kernel-compiled dfharness sysctl tests, prop_dictionary.c state hooks via guest-only harness.diff (window arm / delayed completer emulation), 6-CPU gated racer attempt; several scheduler iterations of the race harness (spin vs tsleep vs gated barrier) before settling on the deterministic window-state proof.
Verified recommended fix
Two-phase once flag in the kernel _PROP_ONCE_RUN: winner runs f() then stores 2; losers spin (cpu_pause) until 2.
Verdict
Mechanism certain: with the prop_dictionary globals in the exact mid-_PROP_ONCE_RUN winner state (flag set, lockinit/rb_tree_init not yet run), a concurrent first user of the real public API NULL-derefs in _prop_rb_tree_find (Fatal trap 12, VA 0x8, _prop_rb_tree_find+0x18). In-the-wild reachability stays speculative: the once flag is already consumed single-threaded at boot on this guest, and the two 6-thread live-race attempts wedged inside the test harness rather than producing an attributable record. Kernel-panic DoS only; no path to uid=0 (fixed-offset NULL read, nanosecond window).
No comments yet.