# DF-3366 — VERDICT

**Status: reproduced (unprivileged local kernel heap info leak).**
**Impact: leak — uninitialized/stale kernel heap bytes disclosed to any local
user via PF_ROUTE RTM_GET.**
**Confidence: certain.**

## Root cause

1. `rt_setgate()` (sys/net/route.c:1182, 1213-1223) allocates the route's
   key+gateway chunk with `dlen = RT_ROUNDUP(dst->sa_len)` and
   `glen = RT_ROUNDUP(gate->sa_len)` — rounded lengths.
2. `rtrequest1()` RTM_ADD/RTM_RESOLVE then initializes the key with only
   `sa_len` valid bytes:
   * route.c:925 `bcopy(dst, ndst, dst->sa_len)` — nothing initializes
     `[sa_len, RT_ROUNDUP(sa_len))`;
   * route.c:922 `rt_maskedcopy()` — zeroes only up to `cplim2 = dst +
     src->sa_len` (route.c:1295, 1303-1304), again not to the rounded bound.
   The gateway is copied with the *rounded* length `glen` (route.c:1227),
   which also reads up to 7 bytes past the source sockaddr's valid content
   when the adding message's final sockaddr is unpadded (rtsock.c:1023
   checks `cp + sa->sa_len > cplim`, not the rounded bound).
3. `rt_msg_buffer()` (sys/net/rtsock.c:1134-1143) copies
   `RT_ROUNDUP(sa->sa_len)` bytes of every sockaddr into the reply mbuf
   that is delivered to routing sockets.
4. `route_output()` privilege check (rtsock.c:738-740) exempts `RTM_GET` —
   any local user can read the padded bytes.

Consequence: for any route whose key/gateway family has `sa_len % 8 != 0`
(AF_INET6: 28 → 4-byte tail; AF_LINK/AF_MPLS: variable), 1-7 stale heap
bytes per sockaddr are disclosed. The bytes are *recycled M_RTABLE slab
content* — i.e. fragments of previously freed routing objects.

## How it was reproduced (guest `DragonFly dfbsd 6.5-DEVELOPMENT #0 ... X86_64_GENERIC`)

* Baseline: unprivileged `RTM_GET ::1` — pads `00 00 00 00` (boot-time slab
  came from zeroed pages; leak content is benign here, the disclosure channel
  is nonetheless exercised) — `run.baseline.log`.
* Groom (root): 32 RTM_ADDs of host routes with a `sockaddr_in6` carrying
  `sin6_len = 32` and marker `41 42 43 44` at bytes [28..32). `sin6_len` is
  never validated against the family template, so `rtrequest1` copies all 32
  bytes into the 64-byte key chunk (`bcopy(dst, ndst, dst->sa_len)`).
  EEXIST-failed adds also work: the chunk is allocated and freed on the error
  path (route.c:976). Then 32 RTM_DELETEs free the marker chunks; one victim
  route with a normal `sin6_len = 28` sockaddr is added and reuses a chunk
  from the same M_RTABLE size class — its key tail is the stale marker.
* Decisive read: unprivileged user `maxx` (uid 1001) `RTM_GET
  2001:db8:99::1` → DST pad `41 42 43 44`, 5/5 identical runs (`run.log`).

This proves **cross-object stale kernel heap disclosure**: bytes written by
one (privileged) routing operation into a *freed* kernel allocation surface,
unmodified, in another object's route entry, readable by an unprivileged
user. On a multi-user host any user can silently harvest fragments of freed
routing state (addresses, labels, malloc metadata) and, with route churn,
recycled kernel heap generally.

## Notes / adjacent surface (same consumer, not fixed here)

* The `RTA_IFP/RTA_IFA` sockaddrs filled by `_fillrtmsg` are also copied out
  rounded: the link-level `sockaddr_dl` is allocated pre-rounded
  (sys/net/if.c:568, `sdl_len = socksize`), AF_INET is 16 bytes — no tail;
  embedded AF_INET6 ifaddr sockaddrs expose 4 adjacent struct bytes
  (in-allocation, structured data, not stale heap).
* `rt_msg_mbuf()` (rtsock.c:1157+, route-change broadcasts) has the same
  rounded copy; those messages are delivered to all routing sockets.

## Fix validation

* `fix.diff` (in this pack): zero the key padding in `rtrequest1` after the
  key copy, and copy/zero the gateway by exact `sa_len` (also removes the
  ≤7-byte over-read of the source sockaddr). Authored against the read-only
  `sys/` tree; applied to the guest's `/usr/src` copy only.
* Procedure: `vm.sh` guest, `cd /usr/src && patch -p1 < fix.diff && make -j6
  nativekernel KERNCONF=X86_64_GENERIC`, reboot into the patched kernel
  (uname recorded in `fix_validation.log`), re-ran the exact PoC.
* Baseline (stock kernel): DST pad `41 42 43 44`.
* Patched kernel: DST pad `00 00 00 00` — leak gone; groom/victim still
  succeed (route add works, so no functional regression).
* `fix_status: fixed`.

## Known-findings cross-reference (pass 2)

* DF-0397/DF-0398 (rt_setshims): **still unfixed** in this tree; note the
  leak component is broader than the partial-alloc case — `rtfree()`
  (route.c:299-307) and the RTM_DELETE path never free `rt_shim[0..2]` at
  all, so *every* MPLS route deletion leaks its shims.
* DF-0399 (rtredirect ignores rt_setgate): **still unfixed**.
* DF-0400 (rt_fixchange mask OOB): **still unfixed**, still latent (radix
  allocates masks key-sized; no new trigger path found).
* DF-3358 (rtmask_add_global pollution): **still unfixed**; confirmed there
  is *no free path* for the per-CPU route mask trees — `rn_freemask` is only
  called for pf's mask head (sys/net/pf/pf_ioctl.c:3402), so pollution is
  permanent for the lifetime of the booted kernel.

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