# DF-5682 — `prop_number_unsigned()` is the only unguarded prop_number accessor (type-confused OOB read)

## What

`prop_number_unsigned()` (sys/libprop/prop_number.c:379-384) reads
`pn->pn_value.pnv_is_unsigned` **without** the `prop_object_is_number()`
validation that every sibling accessor performs:

* `prop_number_copy`        prop_number.c:364
* `prop_number_size`        prop_number.c:396
* `prop_number_integer_value`        prop_number.c:432
* `prop_number_unsigned_integer_value` prop_number.c:450
* `prop_number_equals`      prop_number.c:463
* `prop_number_equals_integer`       prop_number.c:477
* `prop_number_equals_unsigned_integer` prop_number.c:496

Layout: `struct _prop_number` places `pn_value` at offset **40**
(16-byte `_prop_object` + 24-byte `rb_node`); the bitfield word lives at
+40..43.  `struct _prop_string` is exactly **40** bytes (16 + ptr +
size_t + int).  Calling `prop_number_unsigned()` on a string (or data /
bool — 16 bytes — object) therefore reads **4 bytes entirely past the
heap allocation**:

```
   0                   16                  40
   +-------------------+-------------------+---------+
   | _prop_string      | data/size/flags   | GUARD / adjacent chunk
   +-------------------+-------------------+---------+
                                            ^ pnv_is_unsigned read (+40..43)
```

Kernel reachability today: **none** — `rg` finds zero in-kernel callers
of `prop_number_unsigned()` outside libprop itself (the only kernel
consumers of prop_number creation are kern_udev.c:195,215, create-side
and type-safe).  The defect is a live memory-safety trap one future
caller away from a user-influencible plist type confusion, and it is
present verbatim in the userland `libprop.so` built from the same
source.  Filed as hardening (Info): certain code defect, no demonstrated
kernel impact.

## Reproduce (rig — guard-page heap on the QEMU guest)

`build.sh` compiles the guest's `/usr/src/sys/libprop`
(md5-identical to the audited tree) twice, with `_PROP_POOL_GET`/
`_PROP_POOL_PUT` patched (userland branch of `prop_object_impl.h`) to
place every proplib object allocation flush against a PROT_NONE guard
page — deterministic, ASAN-free boundary detection (guest base cc 8.3
ships no usable ASAN runtime):

* `prop5682_pg`  — guard-page allocator + **stock** `prop_number.c`
* `prop5682_fix` — guard-page allocator + `prop_number.c` with the
  `prop_object_is_number()` guard added (`pfix_number.c` = the exact
  `fix.diff` under test)

`typeconf.c` creates one `prop_string_t`, then:

1. control — `prop_number_integer_value((prop_number_t)str)`: the
   guarded sibling, must return 0 without trapping;
2. probe — `prop_number_unsigned((prop_number_t)str)`: the unguarded
   accessor.

### Expected / observed (run.log, 2026-10-10)

| variant | control | probe |
|---|---|---|
| baseline (stock) | `-> 0 (no trap)` | **SIGSEGV (exit 139)** on the guard page |
| fixed (guard added) | `-> 0 (no trap)` | `-> 0 (NO TRAP)`, exit 0 |

The 4-byte OOB read at +40 is proven deterministically, and the fix is
validated on the same source.

## Fix

`fix.diff` — add the missing guard, matching every sibling accessor:

```diff
 bool
 prop_number_unsigned(prop_number_t pn)
 {
 
+	if (! prop_object_is_number(pn))
+		return (false);
+
 	return (pn->pn_value.pnv_is_unsigned);
 }
```
