# 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.
