# DF-2894 — VERDICT

**Status: REPRODUCED** (impact: panic; conditional code-exec ceiling — see below)
**Fix: VALIDATED** (fixed kernel drops the oversize entry at boot; PoC then gets ENOENT)

## The bug

`kenv_init()` (sys/kern/kern_environment.c:580-588, invoked at `SI_BOOT1_POST`
via SYSINIT at :594) copies every bootloader-supplied string from the static
environment (`kern_envp`, populated at boot from loader metadata —
sys/platform/pc64/x86_64/machdep.c:2669 `MD_FETCH(kmdp, MODINFOMD_ENVP, ...)`,
i.e. anything `/boot/loader.conf` or the loader prompt defines) into the
dynamic kenv table, validating **only the entry count** (`i < KENV_DYNMAXNUM-1`)
— never the per-entry length. This is the one ingestion path with no length
check: `ksetenv()` (:263-266) enforces `namelen ≤ KENV_MNAMELEN(128)` and
`vallen ≤ KENV_MVALLEN(128)`, and `sys_kenv()`'s KENV_SET clamps its copyin to
129 bytes (:156-157); `kenv_init()` has no equivalent.

Every later `kgetenv()` (:229-243) performs

```c
char buf[KENV_MNAMELEN + 1 + KENV_MVALLEN + 1];   /* :231 — 258 bytes, stack */
...
cp = kenv_getstring_dynamic(name, NULL);          /* :237 — ptr to value */
if (cp != NULL) {
        strcpy(buf, cp);                          /* :239 — UNBOUNDED */
```

so a dynamic-table value of ≥ 258 bytes overflows a fixed kernel-stack buffer
with fully attacker-chosen bytes (no NUL constraint, strcpy terminates only at
the value's end).

## The trigger — unprivileged

`sys_kenv()` KENV_GET (:135-136) calls `kgetenv(name)` for any unprivileged
user; `kenv(1)`, `kenv(3)`, and every in-kernel `kgetenv_string()` consumer
reach the same strcpy.

## Reproduction (stock guest, kernel #0, INVARIANTS, -fno-stack-protector)

1. Plant (root/loader-level, once): `audit.smash="<700 A's>"` and control
   `audit.hello="PANGRAM_CONTROL"` in `/boot/loader.conf`; reboot.
2. Verify ingestion: `kenv | grep -c '^audit.smash='` → 1 (DUMP path is safe).
3. As `maxx` (uid=1001): `/tmp/df2894_trigger audit.hello` → returns value,
   exit 0. `/tmp/df2894_trigger audit.smash` → **never returns**; serial console:

```
Fatal trap 9: general protection fault while in kernel mode
frame pointer     = 0x10:0x4141414141414141
current process   = 824        (the unpriv trigger process)
Stopped at      kgetenv.part.4+0xea:    ret
```

Saved RBP == 0x4141414141414141 (the planted bytes); fault on kgetenv's `ret`.
Guest down in DDB. Full capture: panic.txt, run.log.

## Exploit ceiling (honest)

The overflow itself is a clean, byte-controlled kernel-stack smash with no
canary (kernel CFLAGS: `-fno-stack-protector`), no SMEP/SMAP/KASLR on this
guest — i.e. RIP control and a ROP path to `uid=0` exist **once the oversized
entry is in the table**. But planting the entry requires root
(`/boot/loader.conf`), loader-prompt, or boot-media control. So this is *not*
an unprivileged-only privesc; it is:

- a boot-time data → ring-0 corruption boundary crossing (loader.conf is
  configuration, not code — relevant to verified/secure-boot-style integrity
  models), and
- a persistent "root plants, unprivileged user detonates" kernel-smash
  primitive (panic demonstrated; code-exec plausible with ROP since the
  smashed return address is fully chosen by the planter and detonated by any
  user).

Impact recorded as `panic` (what was demonstrated from an unprivileged
trigger); severity Medium because the plant step is privileged.

## Fix (fix.diff — validated)

Ingest clamp in `kenv_init()` (mirror of ksetenv's limits: drop entries longer
than `KENV_MNAMELEN + KENV_MVALLEN` with a boot warning) plus a defense-in-depth
`strlen(cp) >= sizeof(buf)` guard in `kgetenv()`.

Validated: `vm.sh reset with-src`, patch applied in-guest to /usr/src,
`make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel`, reboot
into kernel #1, replanted the identical loader.conf entries, re-ran the exact
PoC as `maxx`:

- boot log: `WARNING: kenv: oversize entry (713 bytes), ignoring string audit.smash=...`
- `kenv | grep -c '^audit.smash='` → 0; control var unaffected
- trigger → `kenv(KENV_GET, audit.smash): No such file or directory`, exit 1,
  **guest stays up**

Baseline (panic) vs patched (ENOENT, healthy guest): fixed. See fix_run.log,
fix_build.log.

## Not re-reported here (known family)

DF-0120/0121/0122/0123 cover the unpriv env read, the KENV_GET clamp
signedness, the kgetenv_quad shift, and the kernenv_next walk.
