β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5693

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5693 Β· 14 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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_alloc body (prop_bool.c:132-135): take &_prop_bool_true/&_prop_bool_false (constant addresses) and prop_object_retain (atomic inc on po_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 returns false for the TRUE singleton (prop_bool.c:177-180), prop_bool_copy() returns NULL (:158-159), prop_object_release() loads po->po_type->pot_lock (prop_object.c:1085) β†’ NULL-pointer deref β†’ kernel panic.
  • State B β€” po_type stored, pb_value still stale (:115-117 gap): 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_init resets po_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; silent uint32_t wrap on stock kernels (benign for statics β€” _prop_bool_free never 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 by prop_bool.c:210-212 (return true, *obj left NULL β€” pre-initialized at prop_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 goes bad: (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 reach pot_emergency_free as a leaf whose break leaves parent != NULL.
  • (d) externalize β€” _prop_bool_externalize emits 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Rig-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.
↓ fix.diffper-fix-DF-5693

Confirmed kernel references

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 / internalize (e.g. a dm ioctl plist) β€” attacker-chosen timing on MP. A loser of the cmpset runs the _prop_bool_alloc body (prop_bool.c:132-135) against singletons whose po_type/pb_value may not be stored; if the winner is stalled, later loser reads observe po_type==NULL (guard lies, prop_object_release NULL-deref panic at prop_object.c:1085), a typed object with stale pb_value (prop_bool_true()==false for the TRUE singleton), or a refcount reset that underflows in-flight releases (KKASSERT panic on INVARIANTS). The guest kernel was NOT crashed: the once-flag is one-shot per boot and only ~ns wide, so each attempt costs a reboot (hundreds expected β€” infeasible; same classification as DF-5681). The differential rig (guest /usr/src/sys/libprop, md5-identical to the audited tree, userland once swapped for a faithful kernel cmpset-once port) proves it deterministically with a widened window (5/5 per mid-init state: violations, refcount asserts, SIGSEGV) and β€” after fixing a getenv self-widening rig artifact β€” at REALISTIC init length too: 4/300 anomalies, stable across two 300-process batteries (KVM vCPU preemption; bare-metal rate far lower). Control (stock pthread_once) 5/5 survive; fix emulation (SYSINIT bootstrap) 5/5 survive with the window still widened. Low: one-shot-per-boot ns window, panic-or-lying-bool ceiling, no write primitive.