# DF-3328 VERDICT — REPRODUCED (uninitialized kernel-heap byte broadcast in forwarded PERRs)

## Reproduction summary

Guest: stock `X86_64_GENERIC #0` (INVARIANTS), clean `with-src` snapshot.
Harness: shared fwm.ko (steps 31/32 + `kern.fwm_ttl`), same auto-peering
dance as DF-3287/DF-3327.

Sequence per trigger (all remote, from the ESTABLISHED attacker peer):

1. `step 31` — PREP, `origaddr = victim` (propagation block skipped at
   `ieee80211_hwmp.c:1521`), `ttl=1`, `targetaddr = 02:00:00:00:e1:01`,
   `targetseq=0x100` → victim now holds a VALID route for Y with
   `nexthop = attacker` (`hwmp.c:1427-1513`).
2. `step 32` — PERR, `dest_flags = 62` (numerically ==
   `IEEE80211_REASON_MESH_PERR_NO_FI`, hitting the switch-on-wrong-field at
   `hwmp.c:1768`, cf. DF-0353), `dest = Y`, `dest_seq = 0` (→ `hr_seq++`
   branch), `ttl = kern.fwm_ttl > 1` → accepted, `j=1`, forwarded
   (`hwmp.c:1814-1824`).

The forwarded PERR is captured by `fwm_raw_xmit` into `kern.fwm_txlog`
(hex). Frame = 43 bytes; `elemid 132` at offset 26, **TTL at offset 28**.

## Decisive observation

| run | injected rx TTLs | forwarded TTL byte (offset 28) |
|-----|------------------|-------------------------------|
| boot 1 (`run.log`) | 5, 9, 200, 77, 250 | `0xfe` ×5 (correct impl: 4, 8, 199, 76, 249) |
| boot 1 (`run.2.log`) | 3, 100, 55, 240, 9, 88, 150, 21, 66, 111 | `0xfe` ×10 (correct: 2, 99, 54, …) |
| boot 2, fresh `reset with-src` (`run.3.log`) | 5, 90, 12 | `0xfe` ×3 (correct: 4, 89, 11) |
| DF-3327-patched kernel #1 (regression) | 42 | `0xfe` (correct: 41) |

18 forwarded PERRs, 16 distinct rx TTLs, 3 boots, 2 kernels: the emitted TTL
is **never** `rx_ttl − 1` and matches **no byte of any injected frame**. It
is the prior contents of the `M_80211_MESH_PERR` heap block at offset 2
(`0xff` on this kernel) minus the single `pperr->perr_ttl--` at
`hwmp.c:1823` — i.e., one byte of kernel heap memory disclosed over the air
to every mesh neighbor, on every forwarded PERR, at up to ~10 Hz
(`ieee80211_hwmp_perrminint` = 100 ms).

## Root cause

* `hwmp.c:1743-1747` — `pperr = kmalloc(..., M_INTWAIT)` **without M_ZERO**
  (RX-path preq/prep/perr allocations at `:430-439`, `:484-491`, `:529-537`
  all use `M_ZERO`).
* `pperr->perr_ttl` is written by nobody; the only touch is
  `hwmp.c:1823` `pperr->perr_ttl--`.
* `hwmp_send_perr` sets `perr_ie`/`perr_len` only (`:1861-1871`);
  `hwmp_add_meshperr` then writes the garbage-derived byte to the wire
  (`:784` `*frm++ = perr->perr_ttl;`).

## Impact ceiling

1 byte of kernel heap per event (Low). Not attacker-chosen on this kernel
(the block consistently holds 0xff), but it is heap-state-dependent
disclosure; on busier 640-byte-bucket systems the byte varies with whatever
last owned the block.

## Fix

```diff
 		pperr->perr_ndests = j;
-		pperr->perr_ttl--;
+		pperr->perr_ttl = perr->perr_ttl - 1;
 		hwmp_send_perr(vap, broadcastaddr, pperr);
```

plus (defense in depth) `M_ZERO` on the `pperr` allocation. Not
kernel-built/validated separately (fix is a one-line initialization; the
DF-3327 kernel rebuild was the budgeted one) — `fix_status: not_testable`
for this pack, code change is trivially side-effect-free.

Guest reset to clean `with-src` snapshot after the runs.

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