# DF-0017 — fix.diff validation notes

Authored by the runner post-verification against the read-only `sys/` tree
(the master DEV kernel that reproduced the panic), then **built, booted,
and validated** in a single-fix kernel (Phase 8). Never applied to `sys/`.

## Status: VALIDATED — fix closes the bug (cap = 8)

## What the fix does

Caps DMSG circuit-nesting depth at the source — the receive CREATE path —
so a peer can never build the arbitrarily deep parent→child chain whose
teardown overflows the 16 KB LWKT thread stack. This is the root-cause
fix; with the chain bounded, the recursive walks in
`kdmsg_simulate_failure()` (`kern_dmsg.c:1321`) and
`kdmsg_state_dying()` (`kern_dmsg.c:1421`) cannot overflow the stack, so
their existing recursive structure becomes safe.

### Changes

1. **`sys/sys/dmsg.h`** — add `int depth;` to `struct kdmsg_state`.
   `state0` is covered because `kdmsg_iocom_init()` does
   `bzero(iocom, sizeof(*iocom))` (`kern_dmsg.c:113`), so `state0.depth = 0`.
   Every other state is `kmalloc(... M_ZERO)`, so the field defaults to 0.

2. **`sys/kern/kern_dmsg.c`**
   - `#define DMSG_MAX_CIRCUIT_DEPTH 8` near the top (see "Why 8" below).
   - **Receive CREATE path** (`kdmsg_state_msgrx`, after `pstate` is
     resolved from `msg->any.head.circuit`): if
     `pstate != state0 && pstate->depth >= DMSG_MAX_CIRCUIT_DEPTH`, log
     `circuit nesting too deep (%d)` and `break` with `error = EINVAL`.
     The check sits *before* `iocom->freerd_state` is consumed (line 898),
     so a rejected CREATE returns cleanly through the `done:` path
     (`error != 0` → skip state update at `:1071`, no state leak) and
     `freerd_state` remains available for reuse.
   - Set `state->depth` in the receive CREATE path (right after
     `state->parent = pstate;`) and in the transmit CREATE path of
     `kdmsg_msg_alloc()` (`:1799`) so a transmit-created state used as a
     circuit parent (via `DMSGF_REVCIRC` lookups in `statewr_tree`)
     carries an accurate depth and the receive-side cap cannot be bypassed.

## Why depth = 8 (NOT 32) — empirically derived

The finding's `## Recommended fix` and the first cut of this `fix.diff`
both proposed `DMSG_MAX_CIRCUIT_DEPTH = 32`. **Phase-8 validation proved
32 is too high** — a single-fix kernel built with cap=32 was driven by
the same PoC; the cap fired correctly (33 states, CREATE #34 rejected
with `circuit nesting too deep (33)`), but the connection-close teardown
**STILL double-faulted** with the identical stack-exhaustion signature.

Root cause of the underestimate: the finding assumed ~50–64 B per nesting
level (≈1–2 frames). That is wrong because
**`kdmsg_state_abort()` re-enters the receive path** —
`kdmsg_state_abort` (`kern_dmsg.c:1404`) calls
`kdmsg_msg_receive_handling(msg)` → `kdmsg_state_msgrx` →
`kdmsg_state_cleanuprx` → `kdmsg_simulate_failure`. So each nesting level
is a ~5-function call cycle, not 1–2 frames, and the real per-level cost
is ~485 B. 33 levels × ~485 B ≈ 16 KB → overflow, exactly as observed.

`DMSG_MAX_CIRCUIT_DEPTH = 8` gives a worst-case chain of 9 states
(depths 0–8) → ~9-level recursion ≈ 4.4 KB, well within the 16 KB stack
with a >3× margin. Legitimate DMSG/HAMMER2 circuit nesting is 1–3, so 8
remains generously non-restrictive.

## Validation (Phase 8)

| step | result |
|------|--------|
| `git apply --check findings/poc/DF-0017/fix.diff` | RC 0 |
| `patch --dry-run -p1` on guest `/usr/src` | all 4 hunks succeeded |
| `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (single-fix) | `NK_DONE rc=0`, no errors (`fix_build.log`) |
| installed `kernel.stripped` → `/boot/kernel/kernel`, rebooted | `kern.version` → `#1` (Thu Jul 2 09:28:57 UTC 2026) |
| PoC `./trigger 300` on unpatched `#0` | **panic** — `Fatal double fault`, page-aligned rsp, DDB (`panic_baseline.txt`) |
| PoC `./trigger 300` on patched `#1` (cap=8) | **no panic**, `TRIGGER_EXIT=0`, guest stays up; boot.log shows `circuit nesting too deep (9)` + 290× `missing parent`; 0 double faults |
| PoC `./trigger 5000` on patched `#1` | **no panic** — cap bounds regardless of attacker input |
| 4 consecutive deep runs on patched `#1` | all clean — fix is deterministic |

## Scope note (defense-in-depth follow-up, NOT required to close the bug)

The finding's `## Recommended fix` also suggests converting the two
recursive `subq` walks to iterative (explicit-stack) traversals as
defense-in-depth. With the depth cap enforced at 8, those walks are
bounded (≤9 levels) and cannot overflow, so this fix does **not** rewrite
them — converting `kdmsg_simulate_failure` in particular is non-trivial
because its `state->scan` / `goto again` logic handles concurrent list
mutation during traversal (`kdmsg_state_abort` can remove elements), and
a faithful iterative rewrite risks introducing new bugs. The cap is the
minimal, root-cause fix; an iterative rewrite remains a worthwhile
hardening follow-up (and would let the cap be raised if a legitimate use
case ever needs deeper nesting).

This runner-authored `fix.diff` **supersedes** the finding markdown's
proposed diff (which used a bound of 32 — too high, empirically still
overflows — and placed the cap after `freerd_state` consumption in one
reading, leaking the freerd state on rejection; this version sits before
the consumption and uses the empirically-safe bound of 8).
