# DF-2227: unbounded `while (*cp != '"')` scan in XML attribute-value parser

## Bug
`_prop_object_internalize_find_tag` (`sys/libprop/prop_object.c:484-485`) scans an
XML attribute value with `while (*cp != '"') cp++;` — **no NUL check in the loop
body**. Every other scanner in the same function uses `_PROP_ISSPACE` (which
includes `_PROP_EOF`, i.e. stops at NUL); only this loop uses the bare `*cp != '"'`
test. The kernel copyin path NUL-terminates at exactly `buf[pref_len]`
(`prop_kern.c:404`), so an attribute value that opens a `"` but never closes it
(`<plist version="X`, `pref_len == strlen`) drives the scan **past the NUL
sentinel** (0x00 ≠ 0x22) into adjacent kernel heap.

## Trigger (privileged)
Only privileged callers reach `prop_dictionary_copyin_ioctl()` on the default
GENERIC guest. This PoC uses `/dev/udev UDEVPROP` (root:wheel 0600 — the only
caller compiled into the base kernel). See VERDICT.md / DF-2231 for the full
reachability analysis (all four callers are privileged).

## Build / Run
```
cc -O2 -o oob_read_poc oob_read_poc.c   # or: ./build.sh
./run.sh            # as root; default pref_len=60000 -> kernel panic
./run.sh 17         # small pref_len -> silent OOB read -> clean EIO
```

## Expected
- **Unpatched kernel (#0)** with `pref_len=60000` (large → kmem/page alloc):
  kernel **panic** — page fault in the buggy loop itself:
  ```
  Fatal trap 12: page fault while in kernel mode
  fault virtual address = 0xfffff8011864f000   (page-aligned, 1st byte past buffer)
  Stopped at _prop_object_internalize_find_tag+0x32f: cmpb $0x22,(%rax)
  ```
- **Patched kernel (#1)**: clean `errno=5 EIO` (unterminated quote now detected as
  a parse error at the NUL sentinel), no panic, guest stays up. Well-formed XML
  still parses normally.

## Impact
Privileged kernel DoS (panic via page fault) + latent OOB read. NOT an
unprivileged escalation: root-only reachability (valid hard blocker) AND a
read-only primitive (no corruption to convert — valid hard blocker). The finding's
secondary "heap leak via copyout" is not realised — the parse always fails after
the OOB scan, so no kernel bytes reach userspace.

## Fix
Add the per-iteration NUL check (same idiom the other scanners use):
`while (!_PROP_EOF(*cp) && *cp != '"') cp++;` — see `fix.diff` (validated).
