# DF-5396 VERDICT

**Status: reproduced (memcorrupt / heap overflow via malicious USB device).**
**Fix: validated on the guest (behavioral differential baseline vs patched).**

## Environment

- Host QEMU 7.2.22 (Debian), KVM, DF guest `6.5-DEVELOPMENT #0/#2 X86_64_GENERIC`
- Malicious device: `usbredir_rndis.py` (this pack) — a full usbredir
  protocol device-side implementation serving a RNDIS gadget
  (0525:a4a2, control iface CDC/ACM/0xFF so `urndis_non_huawei_host_devs`
  matches, if_urndis.c:180-184) over `-device usb-redir`. The guest's
  urndis(4) runs its whole control channel (INIT/QUERY/SET) against
  attacker-chosen bytes, and the bulk IN pipe delivers fully crafted
  RNDIS `REMOTE_NDIS_PACKET_MSG` frames.
- cdce must be kept from loading (`if_cdce.ko` moved away): its VPI
  match on 0525:a4a2 wins probe priority and its *own* DF-5334 attach
  panic fires first.

## How the reproduction works

1. `boot_rig.sh` boots the guest clean; `kldload if_urndis`; start
   `usbredir_rndis.py`; `device_add usb-redir` via the QEMU monitor.
   The guest attaches the device:
   `ue0: MAC address: 02:00:de:ad:be:ef` (the attacker device's MAC —
   proof the whole RNDIS control channel is ours).
2. `ifconfig ue0 up` → RNDIS INIT handshake (our INIT_CMPLT) → bulk
   transfers start.
3. Control plane (TCP 45601): `send <datalen> <dataoffset> [count]`
   queues a crafted packet message:
   `rm_type=1, rm_len=8+36+datalen, rm_dataoffset=36, rm_datalen=datalen`
   — a *valid* RNDIS data message by every check in
   `urndis_bulk_read_callback` (if_urndis.c:825-865) except the
   `MCLBYTES` bound at :859, which allows exactly the overflow.

## Observed (baseline, stock if_urndis.ko)

```
ue0  Ipkts  Ierrs  Ibytes
     1      0      1500     (benign warm-up)
     4      0      7641     (1500 + 2046 + 2047 + 2048 — all accepted)
```

The 2047-byte frame copies 1 byte, the 2048-byte frame 2 bytes past
the 2048-byte `kmalloc(M_MBUFCL)` cluster — by construction of the
executed path (m_getcl → m_adj(2) → usbd_copy_out(rm_datalen)),
there is no other allocation in play and no alternative sink.

~2600 additional 2048-frames + guest-side cluster churn (nc/dd UDP
spray) produced **no panic** — expected: DF's slab allocator
(kern_slaballoc.c:1555-1580) threads free 2048-byte chunks through the
`c_Next` pointer stored in the chunk's first word, so the 2 controlled
bytes corrupt the *low 16 bits of a slab freelist link* (or a
neighboring in-use cluster's data). The partial pointer stays inside
mapped slab KVA, so nothing faults — a silent corruption primitive.

## Exploit chain (documented; not driven to uid=0 in this run)

1. Device + unprivileged local helper groom the 2048-byte slab zone:
   hold K clusters (UDP datagrams queued on a socket), free alternating
   ones so slab-freelist chunks with live `c_Next` links sit adjacent
   to the victim rx clusters.
2. One crafted frame with `rm_datalen=2048` overwrites the low 16 bits
   of a free neighbor's `c_Next` with attacker data (the last 2 bytes
   of the RNDIS data payload).
3. On the next slab-layer pop, `z_LChunks` is re-pointed inside a 64 KB
   window; the following kmalloc(2048) returns an attacker-steered
   address. The *next* rx frame then writes up to 2046 controlled bytes
   there (`usbd_copy_out` into the "cluster").
4. With the standard no-KASLR/no-SMAP/SMEP guest image this is a
   kernel write primitive aimed at whatever occupies the steered
   address (e.g. adjacent slab zones holding credential-adjacent
   objects), with the packet content fully chosen by the device.
   Deterministic uid=0 needs per-boot heap reconnaissance (a leak or
   repeated partial-pointer probing); not completed in this session.

## Fix validation

`fix.diff` (mirrors FreeBSD a51f980) applied to the guest's
/usr/src, `if_urndis.ko` rebuilt and installed; same PoC traffic:

```
baseline:  2046/2047/2048 all accepted (Ipkts 4, Ierrs 0)  -> overflow executes
patched:   2046 accepted (Ipkts 1, Ibytes 2046), 2047/2048 rejected (Ierrs 2)
```

No regression: normal attach, INIT/QUERY/SET, and 2046-byte frames
unchanged. The patched module was also exercised with the malformed
descriptor device of DF-5397 (clean ENXIO) — both packs share this
build.

## Negative results (sanity chain audited at runtime)

- `rm_len > actlen`, `rm_dataoffset >= actlen`, `rm_datalen > actlen`,
  `dataoffset+datalen+8 > actlen`, `rm_type != 1`, `rm_len < 44` are
  all correctly rejected (frames dropped, guest unharmed) — the wrap
  attempts (`rm_dataoffset + rm_datalen + 8` overflow) are impossible
  because both fields are first individually bounded by `actlen ≤
  RNDIS_RX_MAXLEN` (16384).
- Multi-packet messages (two valid messages in one transfer) parse
  correctly and terminate.
- TX path: `m->m_pkthdr.len + sizeof(msg) > RNDIS_TX_MAXLEN` correctly
  bounds the write frame; frame offsets `x * RNDIS_TX_MAXLEN` stay
  inside the 8*2048 DMA buffer.

## Kernel references

- sys/bus/u4b/net/if_urndis.c:859 (bad bound), :865-877 (m_getcl +
  m_adj + usbd_copy_out sink)
- sys/kern/uipc_mbuf.c:608-624 (mclmeta_ctor: cluster = kmalloc(2048))
- sys/net/ethernet.h:41 (ETHER_ALIGN 2), sys/sys/param.h:497 (MCLBYTES 2048)
- sys/kern/kern_slaballoc.c:1555-1580 (free-chunk c_Next linkage)
- sys/bus/u4b/net/usb_ethernet.c:578 (the correct shared-helper bound)
- FreeBSD a51f9801806b43f453ead9a43d458f2d5ba541e6 (upstream fix)

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