Kernel _PROP_ONCE_RUN cmpset-once has no completion barrier: first concurrent prop_number use races the interned-tree lazy init (lockmgr on mid-lockinit lock, rbt_ops still NULL)
Summary
In the kernel build the once macro is a bare cmpset: exactly one CPU runs the init but losers do NOT wait - _prop_number_lock can proceed to lockmgr() while lockinit is still overwriting lk_count/lk_lockholder (wiping an acquisition, breaking mutual exclusion on the global interned tree) and _prop_rb_tree_find can deref rbt_ops while still BSS-NULL (NULL indirect call -> panic). Userland safe (pthread_once) - kernel-only, same source. Rig-differentiated: pthread_once control + SYSINIT-warmup variant SURVIVE 5/5, the kernel-macro port SIGSEGVs 5/5 in _prop_rb_tree_find. Kernel first-use window closed at boot on this tree (classified not_reproduced in-kernel); DF-2225 filed the same macro shape on prop_dictionary - this is the number-tree instance with the distinct lockmgr-wipe consequence. SYSINIT-bootstrap-at-SI_SUB_PROP fix proposed (the same point that already creates the pool). DF-5674 twin check CLEAN (pot_lock registered, all free paths serialized).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5681 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.5 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| race_once.c | β | 4.0 KB | view raw | |
| ksem_object_impl.h | β | 13.9 KB | view raw | |
| ksem_number.c | β | 14.9 KB | view raw | |
| build.sh | β | 75 B | view raw | |
| run.sh | β | 99 B | view raw | |
| build.log | β | 87 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| env.txt | β | 228 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-5681 β prop_number lazy-init once-race: kernel _PROP_ONCE_RUN has no completion barrier
What
sys/libprop/prop_number.c lazily initializes its interned-number
registry (_prop_number_tree + _prop_number_tree_mutex) through
_PROP_ONCE_RUN(_prop_number_init_once, _prop_number_init) at
prop_number.c:182 (_prop_number_lock, reached on every number
release via pot_lock) and prop_number.c:277 (_prop_number_alloc).
In the kernel build _PROP_ONCE_RUN is (prop_object_impl.h:302-303):
#define _PROP_ONCE_DECL(x) static int x = 0;
#define _PROP_ONCE_RUN(x,f) if (atomic_cmpset_int(&(x), 0, 1)) f()
This provides "exactly one CPU runs f()" but no completion
barrier: every CPU that loses the cmpset falls through immediately
and proceeds to use the data that f() is still initializing.
_prop_number_init() (prop_number.c:169-176) does
lockinit(&_prop_number_tree_mutex, ...) then
_prop_rb_tree_init(&_prop_number_tree, ...). A losing CPU therefore:
- calls
lockmgr(&_prop_number_tree_mutex, LK_EXCLUSIVE)on a lock that is mid-lockinitβlockinit()overwriteslk_countandlk_lockholder(sys/kern/kern_lock.c:1300-1307), wiping the loser's acquisition state, so mutual exclusion on the interned tree can be broken from birth; and - calls
_prop_rb_tree_find()(prop_number.c:284) on a tree whoserbt_opsis still the BSS-zero NULL β_prop_rb_tree_findunconditionally dereferencesrbt->rbt_ops->rbto_compare_key(sys/libprop/prop_rb.c:90-93) β NULL-pointer indirect call β kernel panic.
Window: only the first-ever concurrent prop_number use system-wide
(two CPUs entering _prop_number_alloc β or create+release β at the
same instant, e.g. two simultaneously-serviced proplist ioctls or udev
events before any other proplib number traffic). The window is the
duration of lockinit+_prop_rb_tree_init (sub-microsecond), so this
is a narrow, boot-time-flavored race: realistic worst case is a panic;
the broken-mutex consequence would additionally allow two CPUs inside
the rb-tree critical section (same corruption class as DF-5674), but
that combination is considered theoretical.
The userland build is not affected: there _PROP_ONCE_RUN ispthread_once` (prop_object_impl.h:428-430), which does block losers
until initialization completes. Same source, divergent macro β the
defect is kernel-only.
Reproduce (rig β differential harness on the QEMU guest)
The guest's /usr/src/sys/libprop is md5-identical to the audited tree
(db1cdaf5e03235d1a1c4a3e2c4a2720b). build.sh compiles two userland
libprop variants from it:
prop5681_origβ stock sources (pthread_once). Control.prop5681_ksemβ identical sources except the userland once-macros are replaced by a faithful port of the_KERNELcmpset-once (__sync_bool_compare_and_swap), and_prop_number_initcarries a 50 ms window-wideningusleepbetweenlockinitand_prop_rb_tree_init(instrumentation only; it widens exactly the ordering window the kernel macro leaves open).
run.sh then runs:
./race_once(control) β 8 threads barrier-synchronized, each doing first-everprop_number_create_integer+ interning-invariant checks../race_once_ksemβ same, against kernel-macro semantics../race_once_ksem warmupβ same ksem library, but the once is completed single-threaded before the barrier: exactly what the proposedSYSINIT(SI_SUB_PROP)bootstrap fix does in-kernel.
Expected / observed (run.log, 2026-10-10)
| variant | result |
|---|---|
| control (pthread_once) | SURVIVED |
| kernel-macro semantics | SIGSEGV, 5/5 runs (exit 139) β losers reach _prop_rb_tree_find while the winner is still in _prop_number_init |
| fix emulation (once pre-completed) | SURVIVED, 5/5 runs |
Bottom line: with the kernel's once-macro semantics the first-use race
is deterministically fatal in the rig; completing the once before first
concurrent use (the SYSINIT fix) eliminates it. The running guest
kernel itself was not crashed (its first-use window closed during
boot long before login), so the kernel-binary manifestation is
classified not_reproduced β the pack proves the mechanism.
Fix
fix.diff β bootstrap the once single-threaded at SI_SUB_PROP
(the same SYSINIT point that already creates the number objcache pool,
_PROP_POOL_INIT, prop_object_impl.h:273-281 β any first allocation
necessarily comes after it, so the ordering assumption is already
load-bearing in-tree). No hot-path cost; the lazy cmpset stays as a
no-op backstop.
DF-5681 β VERDICT
Status: not_reproduced (kernel binary) β mechanism proven deterministically
in a faithful rig; defect certain from code inspection.
The finding
_PROP_ONCE_RUN in the kernel build (prop_object_impl.h:302-303) is a
cmpset-once with no completion barrier:
#define _PROP_ONCE_RUN(x,f) if (atomic_cmpset_int(&(x), 0, 1)) f()
prop_number.c runs it at :182 (_prop_number_lock, on the hot path of
every number release via pot_lock) and :277 (_prop_number_alloc).
The losing CPU of the cmpset does not wait for the winner's
_prop_number_init() (prop_number.c:169-176) and can therefore:
lockmgr()astruct lockthat is mid-lockinitβ andlockinitoverwriteslk_count/lk_lockholder(sys/kern/kern_lock.c:1300-1307), destroying the loser's acquisition β mutual exclusion on the interned number tree broken from birth (two CPUs may then concurrently mutate_prop_number_treeβ the DF-5674 corruption class); and/or- reach
_prop_rb_tree_find(prop_number.c:284) while_prop_number_tree.rbt_opsis still the BSS-zero NULL β_prop_rb_tree_findloadsrbt->rbto_compare_key(sys/libprop/prop_rb.c:90-93) β NULL-pointer indirect call β panic.
The twin check requested for this file: prop_number does register
pot_lock/pot_unlock (prop_number.c:84-85) and both
prop_object_release (prop_object.c:1077-1097) and
prop_object_release_emergency (prop_object.c:1017-1043) hold them
across pot_free β the DF-5674 defect does not apply to numbers.
The once-race is a different, kernel-only defect in the same lazy-init
machinery: userland compiles _PROP_ONCE_RUN to pthread_once
(prop_object_impl.h:428-430), which blocks losers until completion β
so libprop.so is safe and only the kernel macro is broken.
Why not reproduced
The kernel manifestation requires winning a sub-microsecond window at
the first-ever prop_number use system-wide. On the running guest
that window closed during early boot (proplib numbers are created by
udev/attach traffic long before login), and no unprivileged runtime
action can re-open it. Crashing the guest kernel was therefore not
possible; the classification follows the honest mapping (guest never
panicked β not_reproduced).
What WAS demonstrated (run.log)
Rig on the QEMU guest (sources: guest /usr/src/sys/libprop,
md5-identical to the audited sys/libprop β verified):
- Control β stock userland libprop (pthread_once): 8 threads,
barrier-synchronized first-use, 512 values each, interning invariant
checked:
SURVIVED, exit 0. - Kernel-macro semantics β same sources, userland once swapped for
a faithful port of the
_KERNELcmpset-once, plus a 50 ms window-wideningusleepinside_prop_number_initbetweenlockinitand_prop_rb_tree_init(the exact ordering the kernel macro leaves unprotected): SIGSEGV 5/5 runs (exit 139) β losers proceed into_prop_rb_tree_findwithrbt_ops == NULLwhile the winner is still initializing. - Fix emulation β same ksem library, once completed
single-threaded before first concurrent use (what the
SYSINIT(SI_SUB_PROP)bootstrap does): SURVIVED 5/5, exit 0.
The differential (safe β crash β safe with only the once-completion property changed) isolates the defect to the missing completion barrier exactly as analyzed.
Fix validation
fix.diff bootstraps the once at SI_SUB_PROP β the same SYSINIT point
that already creates the number objcache pool (_PROP_POOL_INIT,
prop_object_impl.h:273-281), so "no consumer runs before it" is an
in-tree guarantee the pool already relies on. Rig-level fix control
(variant 3 above) is the emulation of exactly this fix and survives
5/5. A single-fix kernel rebuild was not performed (no kernel-memory-
corruption reproduction to gate it); fix_status fixed is claimed at
rig level + ordering-equivalence argument, noted accordingly.
Kernel refs
- sys/libprop/prop_number.c:84-85, 151-152, 167, 169-190, 272-320
- sys/libprop/prop_object_impl.h:273-281, 290-293, 302-303, 428-430
- sys/libprop/prop_rb.c:90-93
- sys/kern/kern_lock.c:1300-1307
- sys/libprop/prop_object.c:1017-1043, 1077-1097
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
fixedRig-level fix validation: baseline (kernel once-macro semantics) SIGSEGV 5/5; fix emulation (once completed before first concurrent use = exactly the SYSINIT bootstrap of fix.diff) survives 5/5 with interning invariant intact. Single-fix kernel rebuild not performed: no kernel memory-corruption reproduction gates it, and the fix's ordering guarantee (consumers run after SI_SUB_PROP) is the same in-tree guarantee the existing _PROP_POOL_INIT SYSINIT already relies on.
run.log section 3 (fix runs 1-5 exit=0, 'SURVIVED: interning invariant held') vs section 2 (ksem runs 1-5 exit=139); fix.diff adds the SI_SUB_PROP SYSINIT bootstrap to sys/libprop/prop_number.c.
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log lines: 'control exit=0' / '--- ksem run1..5 exit=139' (SIGSEGV with kernel once-macro semantics) / '--- fix run1..5 exit=0'; VERDICT.md traces the macro at prop_object_impl.h:302-303 vs :428-430 and the crash path prop_number.c:277->283->284 + prop_rb.c:90-93; fix.diff = SI_SUB_PROP SYSINIT bootstrap.
PoC changes
Authored fresh (no seed): race_once.c harness; ksem_object_impl.h (userland once swapped for a faithful port of the kernel cmpset-once) and ksem_number.c (50ms window-widening usleep inside _prop_number_init, instrumentation only) generated from the md5-identical guest sources.
Verified recommended fix
Bootstrap the lazy init single-threaded at SI_SUB_PROP via SYSINIT so the cmpset-once can never race first use (see fix.diff); alternatively give the kernel _PROP_ONCE_RUN real completion semantics.
Verdict
Kernel-only lazy-init race: _PROP_ONCE_RUN compiled for the kernel (prop_object_impl.h:302-303) is a cmpset-once with no completion barrier, so the first-ever concurrent prop_number use lets the cmpset loser reach lockmgr()/_prop_rb_tree_find() while the winner is still inside _prop_number_init() (prop_number.c:169-176) - lockinit overwrites the loser's lock state (kern_lock.c:1300-1307) and rbt_ops may still be NULL (prop_rb.c:90-93 NULL indirect call -> panic). Userland is safe (pthread_once). The guest kernel itself was NOT crashed (the sub-microsecond first-use window closed during boot), hence not_reproduced; the rig proves the mechanism deterministically: stock userland libprop survives 8-thread barrier first-use, the same sources with the kernel cmpset-once port SIGSEGV 5/5 in _prop_rb_tree_find, and pre-completing the once (the SYSINIT fix) survives 5/5. Realistic kernel worst case is a boot-time-flavored panic; broken-mutex rb-tree corruption is theoretical. Fix: bootstrap the once at SI_SUB_PROP (same point the number objcache pool is created), validated at rig level.
No comments yet.