# 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):

```c
#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:

1. calls `lockmgr(&_prop_number_tree_mutex, LK_EXCLUSIVE)` on a lock
   that is mid-`lockinit` — `lockinit()` *overwrites* `lk_count` and
   `lk_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
2. calls `_prop_rb_tree_find()` (prop_number.c:284) on a tree whose
   `rbt_ops` is still the BSS-zero NULL — `_prop_rb_tree_find`
   unconditionally dereferences `rbt->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 is
`pthread_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 `_KERNEL` cmpset-once
  (`__sync_bool_compare_and_swap`), and `_prop_number_init` carries a
  50 ms window-widening `usleep` between `lockinit` and
  `_prop_rb_tree_init` (instrumentation only; it widens exactly the
  ordering window the kernel macro leaves open).

`run.sh` then runs:

1. `./race_once` (control) — 8 threads barrier-synchronized, each doing
   first-ever `prop_number_create_integer` + interning-invariant checks.
2. `./race_once_ksem` — same, against kernel-macro semantics.
3. `./race_once_ksem warmup` — same ksem library, but the once is
   completed single-threaded before the barrier: exactly what the
   proposed `SYSINIT(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.
