# DF-5674 — prop_dictionary keysym free mutates global rb-tree without the tree mutex

## What

`sys/libprop/prop_dictionary.c` — the interned dictionary-key symbol
(`prop_dictionary_keysym_t`) registry is a process-global red-black tree
`_prop_dict_keysym_tree` (prop_dictionary.c:203) guarded by
`_prop_dict_keysym_tree_mutex` (prop_dictionary.c:206).

Every code path that *looks up or inserts* into that tree takes the mutex
(`_prop_dict_keysym_alloc`, prop_dictionary.c:294-341), and the keysym frees
that happen inside a *dictionary* free are covered only because the
dictionary object type's `pot_lock` happens to be the same global mutex
(prop_dictionary.c:134-135, taken by `prop_object_release`,
sys/libprop/prop_object.c:1077-1078).

But the **keysym object type itself registers no lock**
(prop_dictionary.c:148-153 — `_prop_object_type_dict_keysym` has no
`.pot_lock`/`.pot_unlock`). Whenever a keysym's refcount drops to zero
outside a dictionary-type free, `_prop_dict_keysym_free()`
(prop_dictionary.c:233-242) runs `_prop_rb_tree_remove_node()` on the
global tree with **no mutex held**, racing concurrent locked
find/insert/remove from any other thread. Keysyms are interned process- or
kernel-globally, so *any* two dictionaries that use the same key string
share one keysym object — the race cross-cuts unrelated dictionaries.

Unprotected release paths in this file:

* `prop_dictionary_remove()` → `_prop_dictionary_remove()` releases the
  keysym at prop_dictionary.c:1104 while holding only the *dictionary's
  own* rwlock (WRLOCK at :1123), never the tree mutex.
* `prop_dictionary_set()` error path releases a freshly allocated keysym
  at prop_dictionary.c:1009 (expand failure — reachable in userland on
  malloc failure).
* keysyms returned into an array by `prop_dictionary_all_keys()`
  (prop_dictionary.c:843-871) are freed via array release — the array
  type also has no `pot_lock` (prop_array.c:72-80).
* `prop_object_release_emergency()` calls `pot_emergency_free` after
  unlocking (prop_object.c:1042-1051) → `_prop_dictionary_emergency_free`
  releases a keysym unlocked (prop_dictionary.c:411-423).

Consequence: unsynchronized red-black tree rebalancing on live nodes →
torn pointer reads / use-after-free of `rb_node`s → memory corruption.

## Same source, two victims

* **Userland**: `lib/libprop` compiles these exact sources into
  `libprop.so`. Any multithreaded program mixing
  `prop_dictionary_remove`/`all_keys` with `prop_dictionary_set` on
  *any* dictionaries (keysyms are global) can corrupt its own heap.
* **Kernel**: `sys/libprop` is compiled into the kernel; the plist
  internalize path (`prop_dictionary_copyin` ← `sys_vquotactl`,
  sys/kern/vfs_quota.c:346, unprivileged) inserts keysyms under the
  mutex on every parse. Today no in-tree caller invokes
  `prop_dictionary_remove` (the only caller chain
  `udev_dict_delete_key` → `_udev_dict_delete_key`, sys/kern/kern_udev.c:233,405,
  is dead code — zero callers), so the kernel race is currently
  *latent*: one new driver call away from unprivileged-user-triggerable
  kernel heap corruption. Reported as a defect with latent kernel
  impact + live userland impact.

## Files

* `race_keysym.c` — minimal reproducer (3 removers + 2 allocers, 96 shared keys).
* `race_attr.c` — same, with SIGSEGV handler that names the faulting symbol (dladdr).
* `race_probe.c` — same, plus `DF5674_unlocked_frees` counter fed by a
  trylock probe compiled into the instrumented library copy
  (`proprop_dictionary.c`): counts keysym frees executed while the tree
  mutex is *not* held.
* `build.sh` / `run.sh` — exact commands used on the DragonFly guest.
* `fix.diff` — the fix validated in this pack (keysym type takes the
  global lock; userland static proplib mutexes become recursive for the
  nested free path).

## Reproduce (on the DragonFly guest)

```
sh build.sh     # builds baseline/probe/fixed libs + harnesses from /usr/src/sys/libprop
sh run.sh       # baseline: SIGSEGV in _prop_rb_tree_remove_node (5/5)
                # probe:    SIGSEGV + DF5674_unlocked_frees>0
                # fixed:    10/10 runs survive
```

Expected decisive output (baseline):

```
SIGSEGV in _prop_rb_tree_remove_node (/tmp/proporig/libprop_orig.so) addr=0x0
SIGSEGV in _prop_rb_tree_insert_node (/tmp/proprop/libprop_probe.so) addr=0x8   <- the LOCKED inserter, victim
DF5674_unlocked_frees=1145                                                       <- the UNLOCKED remover, culprit
```
