# DF-3759 — VERDICT

**Status: source-level certain; runtime honestly inconclusive (hardware-gated
— no Atheros NIC on the audit guest). reproduced=0, impact=none-on-this-guest;
class ceiling when it fires on real hardware: kernel heap info leak (up to
~14.3KB OOB read per frame) or panic (negative m_len).**

## Root cause (path:line)

1. `sys/dev/netif/alc/if_alc.c:3302-3305` — `alc_rxeof()` parses the Rx
   Return Structure that the *controller* DMA'd:
   `status`, `rdinfo`, `nsegs = RRD_RD_CNT(rdinfo)` (4 bits, 0..15),
   `rx_cons = RRD_RD_IDX(rdinfo)` (12 bits).
2. `sys/dev/netif/alc/if_alc.c:3307` —
   `sc->alc_cdata.alc_rxlen = RRD_BYTES(status)`; `RRD_BYTES` masks a
   **14-bit** field (`sys/dev/netif/alc/if_alcreg.h:1240-1241`,
   `RRD_LEN_MASK 0x00003FFF`) → 0..16383, fully device-controlled.
3. The frame data itself was DMA'd into at most `nsegs` buffers of
   `sc->alc_buf_size = RX_BUF_SIZE_MAX = 1536` bytes
   (`sys/dev/netif/alc/if_alcreg.h:554`, programmed at
   `sys/dev/netif/alc/if_alc.c:3653-3654`), each backed by an MCLBYTES
   (2048-byte) cluster allocated in `alc_newbuf`
   (`sys/dev/netif/alc/if_alc.c:3187-3190`).
4. **No bound is ever enforced between (2) and (3):**
   * `sys/dev/netif/alc/if_alc.c:3368-3369` —
     `m->m_pkthdr.len = alc_rxlen - ETHER_CRC_LEN;`
   * `sys/dev/netif/alc/if_alc.c:3372-3373` —
     `mp->m_len = alc_rxlen - (nsegs - 1) * sc->alc_buf_size;`
   * `sys/dev/netif/alc/if_alc.c:3385-3386` — nsegs==1:
     `m->m_len = m->m_pkthdr.len;`
   The mbuf is handed to the stack at `sys/dev/netif/alc/if_alc.c:3400`
   (`ifp->if_input(ifp, m, NULL, -1)`).
5. FreeBSD upstream has the identical defect (verified against FreeBSD head
   `sys/dev/alc/if_alc.c:3640,3701-3715` — same expressions, no bound), i.e.
   this is inherited, not a DFly regression; the audit files it because the
   primitive is identical to the already-filed DF-3604 family.

## Consequences (device-controlled)

* `rxlen=16383, nsegs=1` → `m_len = pkthdr.len = 16379` on a 2048-byte
  cluster → every consumer past `if_input` walks ~14.3KB past the cluster.
  bpf listeners and (more usefully to an attacker) the TCP/UDP receive path
  (`m_copydata` → `uiomove`) copy the "payload" to userland → **kernel heap
  info disclosure to an unprivileged local user** with a socket on the
  interface. No write primitive from this bug alone (the NIC's DMA into the
  posted buffer is bounded by the buffer-size register).
* `rxlen < ETHER_CRC_LEN` → negative `m_pkthdr.len`; dropped by
  `ether_input`'s minimum-length check in most paths, but with `nsegs > 1`
  the CRC-strip branch (`if_alc.c:3375-3381`) computes
  `rxtail->m_len -= (ETHER_CRC_LEN - mp->m_len)` with `mp->m_len` a large
  negative int → **negative `m_len` inside the delivered chain** → mbuf
  invariant violation (m_pullup/m_adj/m_copydata walkers) → panic.
  Modeled deterministically in `trigger.c` / `run.log`: nsegs=15, rxlen=100
  gives last `m_len = -21404` and previous `m_len = -19872`.

## Why not DF-1207 (no re-report)

DF-1207 = the *index* half: `rxdesc[rx_cons]` OOB because `rx_cons`
(12-bit) / `nsegs` are unvalidated (`if_alc.c:3304/:3329`). DF-3759 is the
*length* half: even with perfectly valid indices, the mbuf *metadata*
(`m_len` / `m_pkthdr.len`) is derived from an unbounded device field. Same
descriptor, different sink (mbuf length metadata vs array index), different
primitive (OOB read via the stack vs direct rxdesc[] OOB). Both exist
independently.

## Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)

* Guest has **no Atheros PCI device**: `pciconf -l | grep -i -E
  'atheros|1969'` → `NO_ATHEROS_PCI`; `ifconfig -l` → `vtnet0 lo0`; no
  `hw.alc` sysctl; `if_alc.ko` exists only as an unloadable module file
  (env.txt). `alc_attach`/`alc_rxeof` unreachable at runtime.
* Guest `/usr/src/sys/dev/netif/alc/if_alc.c` is **byte-identical** to the
  audited tree (md5 `fb1a46ef2b2559de4aa5d508e03efcd5`, env.txt).
* `trigger.c` (userland arithmetic model of `if_alc.c:3368-3386`) builds
  and runs on the guest (run.log, `TRIGGER_RC=0`) and prints the exact
  corrupted lengths for the three device-controlled shapes — the OOB
  (>cluster) and negative (<0) cases are demonstrated as arithmetic, the
  DMA precondition is documented in the source header.

## Fix

`fix.diff` (git-apply clean against the read-only tree): drop the frame
when `alc_rxlen > nsegs * alc_buf_size || alc_rxlen < ETHER_CRC_LEN`, using
the file's own error-frame early-return idiom (cf. `if_alc.c:3322-3324`;
on early return the buffers are simply reused in place — the same
mechanism the existing CRC/runt error path relies on).

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