# DF-3466 — VERDICT

**Finding**: `ieee80211_ageq_append()` computes a *relative* age
(`age - M_AGE_GET(head)`, sys/netproto/802_11/wlan/ieee80211_ageq.c:114) and
KASSERTs it is non-negative (:116).  The DWDS staging caller
(sys/netproto/802_11/wlan/ieee80211_wds.c:335) derives the requested age from
`ni->ni_intval`, which the AP copies **unvalidated** from the association
request's listen-interval field
(ieee80211_hostap.c:1966 `lintval = le16toh(...)`, :2130
`ni->ni_intval = lintval`).  An associated station therefore controls the
ages handed to the queue, and they are not monotonic: staging a frame with a
large listen-interval first, then re-associating with a small one and staging
a second frame, drives the relative age negative.

## Reproduced — baseline (stock INVARIANTS kernel)

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), up via
dfbsd-qemu/vm.sh.  `ageqpoc.ko` (this directory) drives the exported ageq API
with exactly the two ages a single attacker station produces
(listen-interval 2000 → age 781; then 1 → age 0):

```
ageqpoc: append #1 age=781 (listen-intval 2000)
ageqpoc: append #1 rc=0 aq_len=1
ageqpoc: append #2 age=0 (reassoc listen-intval 1) -> relative age 0-781 = -781
panic: age -781
Trace: ieee80211_ageq_append+0xc1 <- ageqpoc_modevent <- module_register_init
       <- linker_load_file.part.3 <- linker_load_module
```

Full serial-console capture: `panic.txt`, `run.log`.  The ssh session died
with the panic (kldload never returned) and the guest required a hard reset —
classic panic reproduction.

## Impact

* INVARIANTS/debug kernels: **remote denial of service** — any associated
  station on a DWDS (or mixed DWDS+mesh) vap can panic the AP with two
  staged 4-addr frames around a re-association.  Not unauthenticated: the
  port must be authorized (hostap.c:654-663), but open-network APs authorize
  immediately after (re)assoc.  `hwmp.c:2236` (fixed age 15) hits the same
  KASSERT whenever it stages behind a wds head with >15 remaining.
* production (no INVARIANTS) kernels: the negative age is stored into
  `m_pkthdr.csum_data` (M_AGE_SET) and the frame simply expires one sweep
  early — no memory unsafety.  This bounds severity at Medium.

No escalation chain exists: the primitive is a controlled-constant KASSERT
panic, not memory corruption.  uid0 is not reachable from this bug.

## Fix validation (mandatory for reproduced findings — done)

`fix.diff` (this directory) clamps the relative age at 0 inside
`ieee80211_ageq_append` before the KASSERT, protecting all callers (wds,
mesh, hwmp) at once.  Applied to the guest's /usr/src copy, kernel rebuilt
with `make -j4 nativekernel KERNCONF=X86_64_GENERIC` +
`make installkernel`, rebooted:

* baseline kernel #0: `panic: age -781` (see run.log / panic.txt)
* patched kernel #1 (uname below): identical PoC → `append #2 rc=0 aq_len=2`,
  **no panic**, module unloads cleanly, guest stays up (see run.2.log)

fix_kernel_uname: `DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 13 09:29:00
UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64`

Guest was reset to the clean-source snapshot after validation.

## Recommended hardening (in addition to the clamp)

Validate the association listen-interval at parse time (e.g. reject
lintval==0 / clamp to [1,600]) — several consumers trust ni_intval.

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