prop_bool singleton lazy-init shares the kernel cmpset-once with no completion barrier: concurrent first-ever <true/> internalize can hand out a NULL-typed, stale-valued, or refcount-corrupted TRUE/FALSE singleton (DF-2225/DF-5681 class, third instance)
Summary
The kernel _PROP_ONCE_RUN sets the flag BEFORE running init and losers proceed immediately - and unlike the dictionary/number trees NOTHING consumes prop_bool at boot, so the racy first use is exactly the first-ever <true/>/<false/> internalized, attacker-chosen timing on MP. A stalled winner lets losers observe po_type==NULL (guard lies / release NULL-deref panic), typed-but-stale pb_value, or a refcount reset erasing in-flight retains (underflow KKASSERT). Rig: control 5/5, kernel-once port violates 5/5 widened AND 4/300 at realistic init length (stable across two batteries after fixing a rig artifact), fix emulation 5/5. Kernel-binary repro infeasible in bounded time (one-shot per boot, ~ns window) - not_reproduced in-kernel, mechanism rig-proven, same classification as DF-5681. SYSINIT-bootstrap fix authored (#ifdef _KERNEL guarded so the shared userland build compiles). Internalize laxity/type-guards/externalize all verified clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5693 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race_bool.c | β | 5.5 KB | view raw | |
| ksem_object_impl.h | β | 13.9 KB | view raw | |
| ksem_bool.c | β | 6.4 KB | view raw | |
| build.sh | β | 71 B | view raw | |
| run.sh | β | 95 B | view raw | |
| build.log | β | 229 B | view raw | |
| run.log | β | 7.4 KB | view raw | |
| run.2.log | β | 862 B | view raw | |
| run.first-attempt.log | β | 5.5 KB | view raw | |
| env.txt | β | 203 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| VERDICT.md | β | 7.2 KB | β raw | |
| README.md | β | 2.6 KB | β raw | |
| verdict.json | β | 6.3 KB | view raw |
DF-5693 β prop_bool.c shares the kernel cmpset-once with no completion barrier
File: sys/libprop/prop_bool.c (pass 2, GLM 5.3)
Class: lazy-init race (same macro family as DF-2225 prop_dictionary, DF-5681 prop_number)
Severity: Low Β· Confidence: likely (mechanism certain in rig; kernel window one-shot per boot)
Build (on the DragonFly guest, as root, in /tmp)
sh /tmp/build.sh # builds prop5693_orig (stock, CONTROL) and
# prop5693_ksem (userland once swapped for a faithful
# port of the _KERNEL cmpset-once + env-controlled
# window widening), links race_bool / race_bool_ksem
Run
sh /tmp/run.sh # full 6-section battery (~3 min)
Expected
| Section | Variant | Expected | Observed (run.log) |
|---|---|---|---|
| 1 control Γ5 | stock (pthread_once) | survive | 5/5 exit=0 |
| 2 state A Γ5 | ksem, PROP_WIDEN_MS=50 |
VIOLATION[A:po_type=NULL] + refcount-underflow asserts |
5/5 exit=134/139 |
| 3 state B Γ5 | ksem, PROP_WIDEN2_MS=50 |
VIOLATION[B:value=stale] (typed object, prop_bool_true()==false) |
5/5 exit=134/139 |
| 4 release Γ5 | ksem, PROP_WIDEN_MS=50 release |
SIGSEGV (exit 139) releasing po_type==NULL object |
5/5 exit=139 |
| 5 fix Γ5 | ksem + warmup (= SYSINIT bootstrap of fix.diff) |
survive | 5/5 exit=0 |
| 6 realistic Γ300 | ksem, NO widening, fresh processes | ~0 anomalies (why this is Low, not High) | 4/300 anomalies; run.2.log again 4/300 |
The PROP_WIDEN_MS / PROP_WIDEN2_MS knobs are rig instrumentation only
(read once by a constructor so the un-widened path costs two branches; an
earlier getenv-inside-init version self-widened the window β see
run.first-attempt.log and VERDICT.md Β§"rig artifact").
Guest kernel repro
Not feasible in a bounded run: the kernel once-flag is one-shot per
boot (nothing consumes prop_bool during boot; first use is the first
<true/> ever internalized, e.g. via a dm ioctl plist), and the window
is the duration of _prop_bool_init (β4 stores + 2 calls). The rig
(8 threads racing a realistic-length init) hits at ~1.3% per fresh
process under KVM vCPU preemption; the in-kernel hit probability per
boot is far lower on bare metal, and each attempt costs a reboot.
Same classification as DF-5681 (not_reproduced on the kernel binary,
mechanism proven in the differential rig).
Fix
fix.diff β SYSINIT(SI_SUB_PROP) bootstrap, #ifdef _KERNEL-guarded so
the shared userland libprop build stays intact. Section 5 emulates it
(warmup) and survives with the window still artificially widened.
DF-5693 VERDICT β prop_bool.c kernel cmpset-once race (lazy singleton init)
Bottom line
Mechanism proven, kernel-binary reproduction not feasible in a bounded
run, realistic impact floor is a panic or a lying boolean in a
one-shot-per-boot nanosecond window β Low, base:libs. The kernel flavor
of _PROP_ONCE_RUN (sys/libprop/prop_object_impl.h:302-303) flips the
completion flag before running the init function and gives the cmpset
loser no way to wait. sys/libprop/prop_bool.c is the third user of that
macro in the family (after DF-2225/prop_dictionary and DF-5681/
prop_number), and unlike those two it has no boot-time consumer at
all β zero references to prop_bool_* or the bool util twins exist
outside libprop β so the racy first use is exactly the first
<true/>/<false/> ever internalized by the kernel, i.e. attacker-
chosen timing on an MP box (two concurrent plist-carrying ioctls, e.g.
the dm echo conduit established in DF-5675; on this guest /dev/mapper
is not loaded).
What the race is
- Loser path after a lost cmpset β
_prop_bool_allocbody (prop_bool.c:132-135): take&_prop_bool_true/&_prop_bool_false(constant addresses) andprop_object_retain(atomic inc onpo_refcnt,prop_object.c:987-994). No initialized field is read inside the window. - Mid-init states a loser's later reads can observe if the winner is stalled (vCPU preemption, cold BSS cacheline):
- State A β
po_type == NULL:prop_bool_true()guard fails and returnsfalsefor the TRUE singleton (prop_bool.c:177-180),prop_bool_copy()returns NULL (:158-159),prop_object_release()loadspo->po_type->pot_lock(prop_object.c:1085) β NULL-pointer deref β kernel panic. - State B β
po_typestored,pb_valuestill stale (:115-117gap): object looks typed;prop_bool_true()returns false for the TRUE singleton β a plist boolean that silently lies. - Refcount flavor β the winner's
_prop_object_initresetspo_refcnt=1(prop_object.c:86-93), erasing in-flight loser retains; subsequent releases underflow:_PROP_ASSERT(ocnt != 0)(prop_object.c:1085) /(ncnt != 0)(:993) β KKASSERT panic on INVARIANTS kernels; silentuint32_twrap on stock kernels (benign for statics β_prop_bool_freenever frees,prop_bool.c:64-74).
Rig results (faithful kernel once-semantics in userland, sources
md5-identical to the audited tree β build.log)
| Battery | Result |
|---|---|
| 1. control stock Γ5 (pthread_once has completion barrier) | 5/5 SURVIVED |
2. state A widened (PROP_WIDEN_MS=50) Γ5 |
5/5 VIOLATION (guard failed / asserts / SIGSEGV), exit 134/139 |
3. state B widened (PROP_WIDEN2_MS=50) Γ5 |
5/5 VIOLATION[B:value=stale], exit 134/139 |
| 4. release flavor Γ5 | 5/5 SIGSEGV exit 139 (NULL po_type release) |
5. fix emulation (warmup = SYSINIT bootstrap) Γ5 |
5/5 SURVIVED, window still widened |
| 6. realistic window, 300 fresh processes | 4/300 anomalies (3 refcount-underflow asserts, 1 state-A violation) |
6b. realistic window, second 300 (run.2.log) |
4/300 anomalies (same flavors) |
Rig artifact caught and fixed (honesty note)
The first instrumentation draft called getenv() inside
_prop_bool_init. libc getenv scans environ (microseconds), which
self-widened the "realistic" window and produced a bogus 89/300
anomaly rate (run.first-attempt.log). The final rig reads the knobs
once in a constructor (single-threaded, before the race), so the
un-widened init costs two predicted branches. The stable 4/300 result is
therefore genuine to realistic init length under KVM scheduling jitter
(vCPU preemption stalls the winner mid-init far more often than bare
metal would).
Why Low and not higher
- The kernel window is one-shot per kernel lifetime β the once flag never resets β and only ~ns wide on bare metal; the rig's 1.3%-per- spawn rate is inflated by vCPU descheduling and NΓ8-barrier losers, and each kernel attempt costs a reboot. No amplification, no repeat fire.
- Worst case when hit: NULL-deref panic (DoS) or a wrong boolean value in a rejected/echoed plist. No write primitive, no info leak, no privilege boundary.
- Consistent with the family: DF-2225 and DF-5681 (same macro, wider consequences β rb-tree corruption, lockmgr state wipe) are both Low.
Pass-2 checklist (the rest of the file is clean)
- (a) internalize tag laxity β
<true attr/>/<true></true>are rejected byprop_bool.c:210-212(return true,*objleft NULL β pre-initialized atprop_object.c:647). Both consumers handle the NULL child gracefully: dict continue frees tmpkey and releases the dict (prop_dictionary.c:1285-1301), array continue goesbad:(prop_array.c:807-811) β whole-parse rejection, no leak, no deref. The_PROP_ASSERT(_PROP_TAG_MATCH(ctx,"false"))(prop_bool.c:217) is unfalsifiable: the internalizer table matches tags exactly (len+memcmp,prop_object.c:596-597,631-638). - (b) type-guard discipline β all three accessors guard
(
prop_bool_copy:158,prop_bool_true:177,prop_bool_equals:190); no unguarded sibling exists (unlike DF-2229/DF-5682). prop_bool.h has no inline accessors. - (c) singleton semantics β over-release self-heals (statics never
freed; the "leaked initial reference" is by design,
prop_bool.c:64-74); refcount-underflow asserts fire only on a buggy over-releasing consumer; the emergency-free first-iteration NULL-parent hazard is DF-5677 (prop_object.c), and a bool can only reachpot_emergency_freeas a leaf whose break leavesparent != NULL. - (d) externalize β
_prop_bool_externalizeemits only<true/>/<false/>through the bounds-checked append helpers (prop_object.c:147-170); depth is the caller's nesting (bounded by internalize recursion, DF-5675's territory).
Fix validation (rig level, DF-5681 precedent)
Baseline (rig with kernel once semantics): sections 2-4 reproduce
5/5. Fix emulation (once completed single-threaded before concurrent
first use β exactly what fix.diff's SYSINIT(SI_SUB_PROP) bootstrap
orders): section 5 survives 5/5 with the window still artificially
widened. A single-fix kernel rebuild was 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 proplib SYSINITs already rely on. fix.diff
improves on DF-5681's version by #ifdef _KERNEL-guarding the SYSINIT
so the shared userland libprop build (lib/libprop compiles this very
file with -D_LIBPROP) keeps compiling.
Reproduce
sh build.sh && sh run.sh # sections 1-6, see README.md table
Guest must be the bundled DF 6.5-DEVELOPMENT QEMU image with /usr/src/sys/libprop md5-identical to the audited tree (checked and printed by build.sh: prop_bool.c 1cc7afbcβ¦, prop_object_impl.h 92553511β¦).
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) violates 5/5 per mid-init state and SIGSEGVs on the release flavor; fix emulation (once completed before first concurrent use = exactly the SYSINIT(SI_SUB_PROP) bootstrap of fix.diff) survives 5/5 with the window still artificially widened. 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 existing proplib SYSINITs rely on. fix.diff additionally #ifdef _KERNEL-guards the SYSINIT so the shared userland libprop build (same file, -D_LIBPROP) keeps compiling.
run.log '=== 5. fix emulation' runs 1-5 exit=0 with 'SURVIVED: both singletons typed, values correct' vs '=== 2/3/4' baseline runs exit=134/139; fix.diff adds the guarded SYSINIT(prop_bool, SI_SUB_PROP, SI_ORDER_ANY) bootstrap to sys/libprop/prop_bool.c.
Confirmed kernel references
- sys/libprop/prop_bool.c:40-41
- sys/libprop/prop_bool.c:109-136
- sys/libprop/prop_bool.c:111-124
- sys/libprop/prop_bool.c:132-135
- sys/libprop/prop_bool.c:64-74
- sys/libprop/prop_object_impl.h:302-303
- sys/libprop/prop_object_impl.h:428-430
- sys/libprop/prop_object.c:86-93
- sys/libprop/prop_object.c:987-994
- sys/libprop/prop_object.c:1065-1097
- sys/libprop/prop_object.c:647
- sys/dev/disk/dm/device-mapper.c
Detail
Evidence (decisive lines)
run.log: '=== 2' VIOLATION[A:po_type=NULL] x5 runs exit=134/139; '=== 3' VIOLATION[B:value=stale] x5; '=== 4' release flavor exit=139 x5; '=== 5' fix runs exit=0 x5; '=== 6' tally survived=296 violations=1 signals=3 (of 300); run.2.log same 296/1/3; run.first-attempt.log documents the getenv self-widening artifact (bogus 89/300) and why the constructor knob-read replaced it; build.log md5 identity prop_bool.c=1cc7afbc..., prop_object_impl.h=92553511...; VERDICT.md has the full state machine and the pass-2 checklist negatives.
PoC changes
Authored fresh (no seed): race_bool.c 8-thread first-use harness with check/release/warmup modes; ksem_object_impl.h = stock header with the userland pthread_once branch swapped for a faithful port of the kernel cmpset-once (volatile int + __sync_bool_compare_and_swap, flag set before f()); ksem_bool.c = stock prop_bool.c plus constructor-read PROP_WIDEN_MS/PROP_WIDEN2_MS usleep knobs inside _prop_bool_init (instrumentation only; an earlier getenv-inside-init draft self-widened the realistic window and was replaced β see run.first-attempt.log).
Verified recommended fix
Bootstrap the lazy init single-threaded at SI_SUB_PROP via a #ifdef _KERNEL-guarded SYSINIT so the cmpset-once can never race first use (see fix.diff); the macro-level fix (real completion semantics for kernel _PROP_ONCE_RUN) belongs upstream.
Verdict
Kernel-only lazy-init race, third instance of the broken-once class: _PROP_ONCE_RUN compiled for the kernel (prop_object_impl.h:302-303) is a cmpset-once with no completion barrier, and prop_bool has NO boot-time consumer (zero references outside libprop), so the cmpset window is live at the first-ever
No comments yet.