# DF-5691 verdict — prop_dictionary_util.c `_nocopy` twins (pass 2)

**File:** `sys/libprop/prop_dictionary_util.c` · **Mode:** audit+verify (pass 2 GLM 5.3)
**Rig:** same-source userland libprop builds from `/usr/src/sys/libprop`
(md5 `f17254fc6c6b827538f7c629861ee059` == audit tree) × {plain,
page-guard, fixed}. Guest: DragonFly 6.5-DEVELOPMENT, cc 8.3.

## Bottom line

**REPRODUCED (rig primitives; kernel impact today: none beyond a live,
latent dangle).** The dictionary `_nocopy` twins exported by this file
(`prop_dictionary_get_cstring_nocopy` / `prop_dictionary_set_cstring_nocopy`,
template at prop_dictionary_util.c:183-218, expanded :217-218) carry the
DF-5690 borrowed-pointer class into the kernel with — unlike the array
twin — **26 live kernel call sites** (dm_ioctl.c ×25, tbridge.c ×1). Two
of the dm sites *actively invalidate* the borrowed pointer mid-function:

- `dm_dev_status_ioctl` borrows `uuid` at dm_ioctl.c:397 and replaces the
  UUID key at :409 (`prop_dictionary_set` releases the old object,
  prop_dictionary.c:988-993) — `uuid` dangles from :409 to function exit.
- `dm_table_deps_ioctl` borrows `name`+`uuid` at :596-597 and replaces
  BOTH keys at :619-620 — both borrows dangle.

Line-by-line tracing (this run) confirms neither site *dereferences* the
borrow after the replace — the in-tree dangle is real but latent, one
added `kprintf("%s", name)` / refactor away from a kernel UAF-read. The
set-side twin has **zero** kernel callers today (rg over `sys/`).

## What the rig proved (deterministic, 3/3 identical runs)

**Baseline (guard-page allocator; fork per case; SIGSEGV = trap):**

| case | result |
|---|---|
| T1 dict-replace dangle (dm_dev_status :397→:409 pattern) | **TRAP(SIGSEGV)** |
| T1b both-keys replace (dm_table_deps :619-620 pattern) | **TRAP(SIGSEGV)** |
| T2 dict release while borrowed | **TRAP(SIGSEGV)** |
| T2b `set_cstring_nocopy` → caller frees → proplib-handed ptr read | **TRAP(SIGSEGV)** |
| T2c caller *reuses* buffer → externalize (copyout analog) | **leaks `<string>PWNED-BY-BUFFER-REUSE</string>`** (ps_size ignored, re-read to current NUL) |
| T3 frozen ps_size=15 vs current NUL@8190 → `get_cstring` strcpy | **TRAP(SIGSEGV)** (heap overflow of the ps_size+1 allocation) |
| T4 copy-variant control | exit 0 |
| DML exact in-tree dm sequence **without** post-replace deref | exit 0, dict consistent — *the latent-today proof* |
| NCD alive-read sanity | exit 0 |

**Fixed (fix.diff = kernel-build `prop_string_create_cstring_nocopy`
copies; rig-activated via `-DPROP_RIG_FORCE_NOCOPY_COPY`):** T2b reads
intact `"transient-buffer"` (no trap), T2c externalizes the frozen copy
(exit 3 semantics), T3 copies exactly 15 bytes — while T1/T1b/T2 still
trap, which is the **documented caller-side borrow contract** (the get
side is fixed by caller discipline: retain the parent or use the copy
variant — see the dm hardening note in fix.diff's comment).

## Simultaneously machine-confirmed clean (negative results)

- **(b) get-side numeric guards:** 2517-check differential matrix, **0
  failures**. The intN/uintN guard algebra is byte-identical to the array
  twins (prop_dictionary_util.c:101-110/:128-137 vs prop_array_util.c:
  89-98/:116-122) and exactly matches the derived range contract for
  every width, signedness, and cross-width case; `prop_number_size`
  minimal-width semantics (prop_number.c:392-418) included.
- **(c) DF-2240 signed-creator class is ABSENT here:**
  `prop_dictionary_set_uint##size` uses
  `prop_number_create_unsigned_integer` (prop_dictionary_util.c:167);
  set_uint64(UINT64_MAX)→get_uint64 roundtrips exactly (known-good
  differential row). The array bug was `add_uint`'s signed creator
  (prop_array_util.c:187); no such mismatch exists in this file.
- **(d) misc:** `prop_dictionary_set_and_rel` (:222-232) release dance is
  balanced — `prop_dictionary_set` never retains on a false return
  (prop_dictionary.c:975-1046: every failure `goto out` precedes the
  retain); `get_dict` (:44-54), `get_bool` (:56-70), the copy-variant
  `get_cstring` and all type guards are NULL-safe
  (prop_object_type(NULL)=UNKNOWN, prop_object.c:1114-1121) and
  wrong-type-safe (matrix). No other hazards found in the file.

## Why not kernel-impact today

`dm` is `optional dm` (sys/conf/files:48) — module/`optional dm` only,
`/dev/mapper/control` is root-only in default devfs; `tbridge` is a
LINT64 regression device. The two dangling sites hold the borrow
un-dereferenced. Filed **Low / hardening** (severity per DF-5682/DF-5690
precedent, upgraded one notch from Info because the dangle is created by
live in-tree code, not merely one-caller-away).

## Artifacts

`run.log` (decisive), `run2.log`/`run3.log` (stability, 18/18 OK each,
byte-identical results), `build.log` (3 rigs + md5 provenance),
`df5691.c` (harness), `fix.diff` (validated), `guardalloc.h`,
`env.txt`, `manifest.json`, `verdict.json`.

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