# DF-1292 — age (Attansic L1) RX descriptor-length race → OOB heap read

## Finding
`age_rxeof` at `sys/dev/netif/age/if_age.c:2025` calls `age_newbuf(sc, rxd, 0)`
**before** reading `desc->len` at line 2037. `age_newbuf` (line 2783-2785)
reprograms the descriptor for the next receive and overwrites `desc->len`:
```c
desc->len = htole32((segs[0].ds_len & AGE_RD_LEN_MASK=0xFFFF)
                    << AGE_RD_LEN_SHIFT=0);
```
The post-recycle descriptor's upper 16 bits are zero, so
`AGE_RX_BYTES(le32toh(desc->len))` (which masks `0xFFFF0000 >> 16`) returns 0
for every non-first segment. `pktlen` stays 0, then line 2061 does
`pktlen -= ETHER_CRC_LEN` ⇒ pktlen = -4, so line 2079 computes
`m->m_len = age_rxlen - pktlen = age_rxlen + 4`. For a 9018-byte jumbo frame
the first mbuf (a 2046-byte MCLBYTES cluster) is assigned `m_len = 9022`,
producing a ~6976-byte OOB read when upper layers consume the mbuf.

The attack is **unauthenticated remote**: send a multi-segment jumbo frame to
a victim `age0` interface configured for `mtu 9000`.

## Why we did not reproduce at runtime
The audit guest has only a virtio-net NIC (`virtio_pci0`, vendor 1af4). The
`age` driver attaches only to Attansic L1 (Marvell) Gigabit PCI NICs
(vendor 1969). `kldstat -v` shows `pci/if_age` is compiled into GENERIC but it
has no HW to probe, so it does not attach and `age_rxeof` is never called.

## Source-level confirmation
- `if_age.c:2025` — `if (age_newbuf(sc, rxd, 0) != 0)` called BEFORE reading desc->len.
- `if_age.c:2037` — `mp->m_len = AGE_RX_BYTES(le32toh(desc->len));` reads desc->len AFTER newbuf overwrote it.
- `if_age.c:2784-2785` — `desc->addr = ...; desc->len = htole32((segs[0].ds_len & AGE_RD_LEN_MASK) << AGE_RD_LEN_SHIFT);` — confirmed overwrite.
- `if_agereg.h:601-603` — `AGE_RX_BYTES(x)` extracts bits 16-31; `AGE_RRD_LEN_MASK=0xFFFF0000`, `AGE_RRD_LEN_SHIFT=16`.
- `if_agereg.h:612-614` — `AGE_RD_LEN_MASK=0x0000FFFF`, `AGE_RD_LEN_SHIFT=0` — the WRITE side (in `age_newbuf`) puts the buffer size in the LOW 16 bits ⇒ upper 16 bits are 0 after recycle.
- `if_age.c:2058-2079` — `age_rxlen -= ETHER_CRC_LEN; ...; m->m_len = sc->age_cdata.age_rxlen - pktlen;` — produces the oversize m_len.

Bug is real and the order-of-operations root cause is unambiguous. The driver
author intended `desc->len` to be the receive-side length but recycled the
descriptor too early.

## Realistic impact ceiling
Remote OOB heap read of up to ~7 KB per packet on a host with an `age` NIC
running jumbo frames. Could leak kernel heap contents (info disclosure) and
will very likely panic on INVARIANTS-enabled kernels when `m_len` overshoots
the cluster. Not directly an escalation primitive; with the leaked bytes an
attacker may defeat KASLR-equivalent information, but DragonFly disables
vm.randomize_mmap and there is no kernel-text KASLR here anyway.

## Fix
`fix.diff` snapshots `desc->len` into a local `segsz` **before** the
`age_newbuf` call, and uses `segsz` at line 2037 instead of re-reading
`desc->len`. Adds `int segsz;` to `age_rxeof`'s locals.
