# DF-1487 — wb_encap wb_frag[16] OOB write

## Verdict
**REPRODUCED (source-level harness).** The bug is real; impact ceiling is
kernel heap corruption (12 bytes of fresh write + 4-byte RMW past the end of
`struct wb_txdesc`) on any local user who can route a 16-fragment sub-60-byte
mbuf chain to a `wb(4)` (Winbond W89C840F) NIC. The guest has no Winbond NIC
(`pciconf -lv` lists only virtio + PIIX3), so the kernel code path cannot be
exercised end-to-end; the harness demonstrates the OOB indexing using the
genuine loop/padding logic from `if_wb.c:1221-1285`. fix.diff applies
cleanly and `nativekernel` succeeds (rc=0).

## Mechanism (`sys/dev/netif/wb/if_wb.c`)
1. Lines 1221-1238: `for (m = m_head, frag = 0; m != NULL; m = m->m_next)` —
   fills `wb_frag[frag]` while `frag < WB_MAXFRAGS` (=16).
2. If exactly 16 non-empty mbufs are supplied, the loop exits with
   `frag == 16` and `m == NULL` (because `m->m_next` was NULL).
3. Line 1248: `if (m != NULL)` coalesce branch is then **skipped**, so no
   re-pack into a cluster resets `frag`.
4. Line 1274: `if (total_len < WB_MIN_FRAMELEN)` (=60) is true for any
   16-fragment chain summing to < 60 bytes (e.g. 16 × 1-byte mbufs).
5. Line 1275: `f = &c->wb_ptr->wb_frag[frag]` = `wb_frag[16]` — one past
   the `wb_frag[WB_MAXFRAGS]` array declared at `if_wbreg.h:291`.
6. Lines 1276-1280: write `f->wb_ctl`, `f->wb_data`, `f->wb_status` —
   12 bytes of fresh OOB write. `frag++` makes it 17.
7. Line 1284: `c->wb_lastdesc = frag - 1` = 16.
8. Line 1285: `WB_TXCTL(c) |= WB_TXCTL_LASTFRAG` expands to
   `wb_frag[c->wb_lastdesc].wb_ctl |= ...` = `wb_frag[16].wb_ctl |= ...` —
   a 4-byte OOB read-modify-write.
9. Compounding: `wb_start` at line 1327 ignores the return value of
   `wb_encap`, so even when encap fails the half-corrupted state is published.

Trigger: `sendmsg()` with a 16-entry `iov` of total length < 60 to a UDP
socket bound to the wb NIC, OR `AF_INET SOCK_RAW` with `IP_HDRINCL` and a
short IP header.

## Harness proof (`harness.c`)
Reproduces the genuine fill-loop and padding logic and reports the OOB index:

```
config                  total_len      coalesce?     result
16x1                           16             no       OOB!
16x2                           32             no       OOB!
16x3                           48             no       OOB!
16x4                           64             no  in-bounds
15x1                           15             no  in-bounds
17x1                           17            yes  in-bounds
Buggy configs: 3/6 write past wb_frag[15]
```

## Exploit-chain note
This is a write-capable local primitive on real hardware. QEMU has no
Winbond NIC, so the kernel-side chain cannot be exercised here. On a real
`wb(4)` system the corruption lands in adjacent heap (or the next contiguous
`wb_txdesc` when `c->wb_ptr == wb_tx_list[127]`), so a successful trigger
yields reliable heap corruption / DoS at minimum, with heap-grooming →
controlled write a credible escalation path.

## PoC changes
- Original PoC folder had no source.
- Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md,
  manifest.json.

## Fix
`fix.diff` adds `if (frag >= WB_MAXFRAGS) return(1);` at the top of the
padding branch, refusing the encap when the chain consumed all descriptors
and still needs padding. Matches the finding markdown proposal
("refuse encap when frag>=16 && total_len<60").

## Fix-validation
`patch -p1 --forward` succeeds (hunk #1 at line 1271). `nativekernel`
completes with rc=0 (`fix_build.log`). No run-time exercise is possible
because no Winbond NIC is present on the guest → `fix_status:
"not_testable"`. Diff applies and compiles; changed logic closes the OOB.
