# DF-5699 — prop_dictionary_ingest() hands handlers a borrowed, unpinned object reference

**File:** `sys/libprop/prop_ingest.c` (pass 2, GLM 5.3)
**Severity:** Low (latent in-kernel: zero in-tree callers today; realized UAF in the
same-source userland libprop) · **CWE-416 / CWE-667**

## What

`prop_dictionary_ingest()` (prop_ingest.c:121-159) obtains each value with
`prop_dictionary_get()` (prop_ingest.c:133). That accessor copies the stored
pointer out *without* `prop_object_retain()` and drops the dictionary rwlock
before returning (prop_dictionary.c:933-945, unlock at :944). The walker then
runs the arbitrary driver callback `(*pite->pite_handler)(ctx, obj)`
(prop_ingest.c:152) for unbounded time holding only the dictionary's reference.
Any concurrent drop of the key's last reference — `prop_dictionary_remove()`,
`prop_dictionary_set()` on the same key, or release of the dictionary — frees
the object **while the handler is holding it**: a use-after-free handed
straight to driver callback code. The table walk itself also continues to the
next rule afterwards, so the window repeats for every entry.

## Reachability

- Kernel: `sys/conf/files:2249` compiles prop_ingest.c into every kernel
  (`standard`), and the header is exported — any kld may use the API. Today
  there are **zero in-tree callers** (`rg 'prop_ingest|PROP_INGEST' sys/`
  outside libprop matches only conf/files), so the kernel instantiation is
  latent, not attacker-reachable on a stock system.
- Userland: the identical source builds the base libprop
  (`/usr/include/libprop/prop_ingest.h`), exporting the same hazard to
  multi-threaded programs of any privilege level.

## Reproduce (same-source rig on the DragonFly guest)

```
# on the guest, as unprivileged user maxx
cd /tmp/df5699
sh build.sh          # compiles /usr/src/sys/libprop/*.c into libprop_orig.so
sh run.sh            # control (no free) then experiment (free under handler)
```

Expected:

- Control (spray only, no `prop_dictionary_remove`): handler survives,
  `type 1852662386 -> 1852662386`, `value 0x1337beef00 -> 0x1337beef00`,
  `ingest=true`, exit 0. (1852662386 = 0x6E756D72, the fourcc for
  `PROP_TYPE_NUMBER` in this proplib.)
- Experiment: `killer: prop_dictionary_remove() done` → handler's post-sleep
  `prop_object_type(borrowed)` faults → **exit 138 (SIGBUS), core dumped**.
  Observed on attempt 1 of 1 in 3/3 runs (run.log, run.2.log, run.3.log).

## Fix validation

`fix.diff` adds `prop_object_retain(obj)` before the handler dispatch and
`prop_object_release(obj)` on both exits (prop_ingest.c:152 region).
`fix_build.sh` / `fix_run.sh` rebuild the rig from the same sources with only
that hunk applied: the experiment then prints
`type 1852662386 -> 1852662386`, `value 0x1337beef00 -> 0x1337beef00`,
`ingest=true`, no crash (fix_run.log, fix_run.2.log).

## Files

- `df5699_ingest_race.c` — harness (control/experiment differential)
- `build.sh` / `run.sh` — baseline rig
- `fix_build.sh` / `fix_run.sh` — patched rig (fix_validation)
- `prop_ingest_fixed.c` — the patched copy fix.diff was generated from
- `build.log run.log run.2.log run.3.log fix_build.log fix_run.log fix_run.2.log env.txt`
- `fix.diff` — git-apply-able unified diff against `sys/libprop/prop_ingest.c`
