# DF-3344 — VERDICT

**Status: reproduced** (panic via privileged trigger; unprivileged UAF-read/leak
demonstrated; unprivileged tx survived by luck of stale flags). **Impact:
panic** (with a demonstrated unprivileged kernel-heap disclosure primitive and
a structural UAF-write surface on the tx path).

## Guest

DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026,
X86_64_GENERIC (`options INVARIANTS`, `pseudo-device lagg` compiled in),
stock kernel, QEMU/KVM guest via dfbsd-qemu/vm.sh.

## Root cause (path:line)

* `sys/net/lagg/if_lagg.c:218-220` — `lagg_modevent(MOD_LOAD)` registers
  `lagg_port_ifdetach` on **`ifnet_departure_event`**.
* `sys/sys/eventhandler.h:114-137` — slow handlers are string-keyed;
  registering on a string that nothing looks up silently creates an orphan
  list. It compiles; it never fires.
* `sys/net/if_var.h:724-725` + `sys/net/if.c:958` — the real event is
  `ifnet_detach_event`, fired by `if_detach()`. All sibling subsystems use it:
  `sys/net/bridge/if_bridge.c:609`, `sys/net/pf/pf_if.c:118`,
  `sys/net/vlan/if_vlan.c:377`, `sys/net/tun/if_tun.c:330`.
* Therefore the ONLY automatic port-removal path (`lagg_port_ifdetach` →
  `lagg_port_destroy`, if_lagg.c:924-943) is dead code.
* Destroying a member (tap: `tapdestroy` → `ether_ifdetach` → `if_detach`,
  then `kfree(sc, M_TAP)` at sys/net/tap/if_tap.c:495 with the ifnet embedded
  in `tap_softc`) leaves `sc->sc_ports` holding `lp` with a dangling
  `lp->lp_ifp` **forever** — this is not a race; the UAF is persistent and
  every subsequent lagg operation touches freed memory.

## What was demonstrated on the guest (run.log, panic.txt)

1. **Setup** (root): `lagg0` failover with `laggport tap0 laggport tap1`,
   tap0 = MASTER/primary, IP 10.66.7.1/24.
2. **Baseline** (uid=1001 `maxx`): SIOCGLAGG via `/tmp/laggleak` — ports
   tap0, tap1, clean hex names.
3. **Root destroys tap0**: `ifconfig lagg0` still lists
   `laggport: tap0 flags=1<MASTER>` — no cleanup ran.
4. **Unprivileged UAF read**: `/tmp/laggleak` as maxx still returns port[1]
   named `tap0` — those bytes now live in the freed `tap_softc`
   (`lagg_port2req` → `strlcpy(rp_portname, lp->lp_ifp->if_xname)`,
   if_lagg.c:951). Any user can read this; 16 bytes of freed-heap content are
   copied out per port entry.
5. **Aliasing proof**: root creates tap2/3/4 → objcache hands tap2 the freed
   chunk → the *unprivileged* dump now reports port[1] as **'tap2'**. The
   dangling `lp->lp_ifp` provably aliases whatever object now owns that heap
   chunk — the leak tracks kernel heap reuse in real time, and lagg tx would
   dispatch into the aliased ifnet (cross-interface write/corruption surface).
6. **Unprivileged tx**: 3000 UDP packets via lagg0 — guest survived (stale
   freed-memory flags read "inactive", failover selector fell through to the
   live tap1). The freed-memory reads in `lagg_link_active`
   (if_lagg.c:1571/1583 via `LAGG_PORTACTIVE`, if_lagg.h:155-158) did occur;
   whether it crashes depends on stale content.
7. **Panic** (root trigger): `ifconfig tap2 destroy` (chunk freed a second
   time) then `ifconfig lagg0 laggproto lacp` → `lagg_lacp_attach`
   (if_lagg.c:2085-2086) iterates ports → `lacp_port_create(dangling lp)`
   → `if_addmulti(lp->lp_ifp=freed)` (ieee8023ad_lacp.c:544) → freed ifnet's
   stale `if_ioctl` path → **Fatal trap 12, page fault at 0x3c0,
   `Stopped at tapifioctl+0x158`** (serial console capture in panic.txt).
   Guest down at DDB.

## Exploit chain (as far as taken)

* uid=0 required for the *setup* (lagg/tap creation, member destroy) and for
  the demonstrated panic trigger.
* Post-setup, the **unprivileged** surface is real and persistent: any user
  can (a) read freed/reused kernel heap through SIOCGLAGG port names
  (demonstrated live heap-reuse disclosure), (b) drive tx through the dangling
  primary (freed-memory-dependent crash/corruption), (c) any admin operation
  on the lagg (proto switch, destroy) faults through the dangling port.
* Escalation to uid=0 was NOT developed: the primitive is a persistent
  dangling `lp->lp_ifp` into freed-then-reused heap (type-stable-ish via tap
  churn, arbitrary via same-size-class objects). A full chain would require
  grooming the freed chunk with a victim object and pivoting through the
  lagg's unconditional writes (`lagg_port_destroy` restores
  `if_type/if_ioctl/if_output/if_lagg` at if_lagg.c:763-766 into the dangling
  ifnet) — out of budget for this run; the panic + unprivileged leak are the
  demonstrated ceilings.

## Fix validation

`fix.diff`: registers/deregisters on `ifnet_detach_event`
(if_lagg.c:219/223). Applied inside the guest's /usr/src copy (sys/ tree
untouched), `make nativekernel KERNCONF=X86_64_GENERIC`, rebooted into the
patched kernel, re-ran the exact trigger sequence:
see `fix_validation.log` / `verdict.json:fix_*` fields.

## Note on the earlier "ifconfig hang"

The `ifconfig lagg0 laggproto lacp` command appeared to hang over ssh — that
was the panic stalling the system (trap → DDB on console). `vm.sh status`
→ down confirmed the crash.

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