# DF-4067 — VERDICT

**Status: inconclusive (hardware-gated) · source-level: certain · severity: Medium · bucket: kernleak**

## Root cause

`ti_rxeof()` (sys/dev/netif/ti/if_ti.c:1623-1717) hands the received
frame up with its length taken verbatim from the NIC-DMA'd return-ring
descriptor:

```c
m->m_pkthdr.len = m->m_len = cur_rx->ti_len;      /* if_ti.c:1690 */
...
ifp->if_input(ifp, m, NULL, -1);                  /* if_ti.c:1706 */
```

`ti_len` is a plain `uint16_t` (if_tireg.h:710) — the Tigon firmware
can write any value 0..65535.  The driver never compares it with the
capacity of the buffer that backs the descriptor:

| ring | buffer | usable | firmware-legit max (`rcb->ti_max_len`) |
|------|--------|--------|----------------------------------------|
| std | m_getcl cluster | MCLBYTES(2048) − ETHER_ALIGN(2) = 2046 | TI_FRAMELEN 1518 (if_ti.c:1279) |
| mini | MGETHDR data area | MHLEN − 2 | MHLEN − 2 (if_ti.c:1305) |
| jumbo | driver pool slot TI_JLEN 9024 | 9022 | TI_JUMBO_FRAMELEN 9018 (if_ti.c:1290) |

So even a *legit* firmware is trusted to respect max_len; a faulty or
malicious one (or DMA tampering on the PCIe path) can claim up to 65535
bytes.  Result: an mbuf whose `m_len`/`m_pkthdr.len` exceed its storage.
Every consumer that trusts those fields then reads past the buffer:

- `ETHER_BPF_MTAP`/BPF read-out of the frame,
- `ether_input` → `m_pullup`/`m_copydata` in the protocol stack,
- socket receive copies

→ kernel-heap contents (adjacent clusters, adjacent mbufs, jumbo pool
neighbours — and past the contigmalloc block for large claims on the
jumbo ring) are copied into delivered packets — a kernel info leak to
any local user with a receiving socket on the interface — or the walk
faults on an unmapped tail (panic).  Worst case std path: 65535 bytes
claimed on a 2048-byte cluster = **63489 bytes OOB read** (run.log).

## Why inconclusive at runtime, certain at source

Same boundary as DF-3604/DF-3989/DF-4024/DF-4045/DF-4055 (all
`inconclusive`/`certain`): the audit guest has no Tigon hardware
(env.txt: `pciconf -l` shows only virtio + i440fx; Tigon vendors
0x12ae/0x10b7/0x1385/0x10a9/0x1011 absent), so the trigger — a firmware
that lies in `ti_len` — cannot be instantiated.  The source facts are
not in dispute: there is no clamp anywhere between the descriptor read
(if_ti.c:1636-1638) and the length assignment (if_ti.c:1690), verified
by reading every statement of `ti_rxeof`; `trigger.c` models the
per-ring extents and runs on the guest (run.log, RC=0).

Note the sibling interaction: DF-4066 (ti_idx) and this bug compose —
a single descriptor both picks the buffer *and* declares its length.

## Verification narrative

- trigger.c built+run on guest: prints per-ring usable/legit/claimable
  extents and the two headline numbers (std 63489 B OOB; jumbo 56513 B
  OOB past a 9024-byte slot, slot pool 3,465,216 B so large claims run
  past the contigmalloc region).  RC_DF4067_MODEL=0.
- env.txt: guest `DragonFly 6.5-DEVELOPMENT X86_64_GENERIC`, no Tigon
  devices; guest /usr/src if_ti.c md5 75c1662a... == audited tree.

## Fix

`fix.diff`: introduce per-ring `maxlen` (MCLBYTES−2 / MHLEN−2 /
TI_JUMBO_FRAMELEN−2) and drop (ierror + `m_freem`) any frame whose
DMA'd `ti_len` exceeds it.  The mbuf was already replaced in the ring
by `ti_newbuf_*`, so freeing it is the same as the existing
TI_BDFLAG_ERROR path.  `git apply --check` clean; patched module
builds on the guest with `-Werror` (fix_build.log FIXBUILD_f2_RC=0);
runtime validation needs hardware (fix_status=not_testable).

## Cleared in this pass (adjacent length surfaces)

- TX lengths go mbuf → descriptor (`f->ti_len = m->m_len`,
  if_ti.c:1865); mbuf lengths are kernel-produced, no device input.
- `ti_newbuf_*` buffer/descriptor pairs are consistent (descriptors
  armed with exactly the buffer capacity, if_ti.c:707-716/742-751/
  803-813).
- The mini-ring `rcb->ti_max_len = MHLEN - ETHER_ALIGN` (if_ti.c:1305)
  matches the buffer; only the return-path trust is missing.

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