lagg port-detach handler registered on never-fired ifnet_departure_event: persistent kernel UAF after member interface destroy
Summary
lagg_modevent registers lagg_port_ifdetach on the string key 'ifnet_departure_event'. That key is never EVENTHANDLER_INVOKE'd anywhere in the DragonFly kernel - if_detach() fires 'ifnet_detach_event', which is what every sibling subsystem registers on (bridge, pf, vlan, tun). The FreeBSD heritage event name survived the port; the handler compiles into an orphan list and never runs, making the only automatic port-removal path dead code. Destroying a member interface (e.g. ifconfig tap0 destroy frees the whole tap_softc with the ifnet embedded) leaves lp->lp_ifp dangling into freed heap permanently - not a race. VERIFIED on guest: after tap0 destroy, 'ifconfig lagg0' STILL lists 'laggport: tap0'; unpriv SIOCGLAGG dump still returns port[1]='tap0' read from freed tap_softc; after creating tap2-4 the unpriv dump returns port[1]='tap2' (dangling pointer into the reused chunk - kernel heap disclosure to unprivileged user); 'ifconfig tap2 destroy + laggproto lacp' -> Fatal trap 12 fault va 0x3c0 'Stopped at tapifioctl+0x158', guest down. Every later lagg operation touches freed memory: unpriv TX drives netmsg dispatch into the freed ifnet's ifq (UAF write); admin ops fault through the dangling port. uid0 chain not developed (would need grooming + pivot via lagg_port_destroy's unconditional writes); demonstrated ceiling = persistent unpriv heap disclosure + reliable panic. Fix validated (register on ifnet_detach_event): two-line fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3344 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| laggleak.c | β | 1.6 KB | view raw | |
| udptx.c | β | 1.7 KB | view raw | |
| build.sh | β | 134 B | view raw | |
| run.sh | β | 209 B | view raw | |
| run.log | β | 3.9 KB | view raw | |
| panic.txt | β | 2.2 KB | view raw | |
| fix.diff | β | 539 B | view raw | |
| fix_validation.log | β | 1009 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
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 lagg0as 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()readslp->lp_ifp->if_link_state/if_flagsfrom freed memory, and on an "active" stale port dispatches a netmsg whose targetifpis 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 staleif_ioctlβ kernel panic (demonstrated:Fatal trap 12,Stopped at tapifioctl+0x158). - privileged
ifconfig lagg0 destroy:lagg_port_destroy()writesif_type/if_ioctl/if_output/if_laggrestores 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 commandsrun.logβ full session capture (baseline β destroy β leak β aliasing β panic)panic.txtβ serial-console panic capturefix.diffβ the two-line event-name fixVERDICT.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.
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)registerslagg_port_ifdetachonifnet_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 isifnet_detach_event, fired byif_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, thenkfree(sc, M_TAP)at sys/net/tap/if_tap.c:495 with the ifnet embedded intap_softc) leavessc->sc_portsholdinglpwith a danglinglp->lp_ifpforever β 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)
- Setup (root):
lagg0failover withlaggport tap0 laggport tap1, tap0 = MASTER/primary, IP 10.66.7.1/24. - Baseline (uid=1001
maxx): SIOCGLAGG via/tmp/laggleakβ ports tap0, tap1, clean hex names. - Root destroys tap0:
ifconfig lagg0still listslaggport: tap0 flags=1<MASTER>β no cleanup ran. - Unprivileged UAF read:
/tmp/laggleakas maxx still returns port[1] namedtap0β those bytes now live in the freedtap_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. - 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_ifpprovably 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). - 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 viaLAGG_PORTACTIVE, if_lagg.h:155-158) did occur; whether it crashes depends on stale content. - Panic (root trigger):
ifconfig tap2 destroy(chunk freed a second time) thenifconfig 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 staleif_ioctlpath β 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_ifpinto 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_destroyrestoresif_type/if_ioctl/if_output/if_laggat 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.
Fix verification
fixedBaseline stock kernel #0: stale member after destroy + Fatal trap 12 in tapifioctl via laggproto lacp. Patched kernel #1 (fix.diff applied in-guest, nativekernel rebuild, reboot): ifconfig tap0 destroy cleanly removes the port (unpriv SIOCGLAGG: ports=1, no stale entry, tap1 becomes MASTER); laggproto lacp and roundrobin switches succeed; guest up. Bad behavior gone.
findings/poc/DF-3344/fix_validation.log (full sequence on kernel #1) vs run.log + panic.txt (kernel #0)
Confirmed kernel references
Detail
Exploit chain
root: lagg0 create + laggport tap0/tap1 + up + IP -> root: ifconfig tap0 destroy (dangling port installed; UAF is now persistent) -> unprivileged: SIOCGLAGG loop reads freed-then-reused kernel heap through lp_ifp->if_xname (demonstrated: name flipped tap0->tap2 when objcache reused the chunk) -> unprivileged tx can dispatch netmsgs into the dangling/aliased ifnet (content-dependent; survived in this run) -> privileged admin op on the lagg (laggproto lacp demonstrated) faults through the freed ifnet -> Fatal trap 12 panic. Full uid=0 chain not developed: would require grooming the freed chunk with a chosen victim object and pivoting via lagg_port_destroy's unconditional if_type/if_ioctl/if_output/if_lagg restores into the dangling ifnet (if_lagg.c:763-766); demonstrated ceiling is persistent unprivileged heap disclosure + reliable panic.
Evidence (decisive lines)
['findings/poc/DF-3344/run.log - steps [3]/[4]: destroyed tap0 still listed (root+unpriv), post-destroy hex name from freed heap', "findings/poc/DF-3344/run.log - step [6]: unprivileged dump flips port[1] 'tap0'->'tap2' after tap2 create (dangling pointer into reused chunk)", "findings/poc/DF-3344/panic.txt - 'Fatal trap 12 ... fault virtual address = 0x3c0 ... Stopped at tapifioctl+0x158' reached via laggproto lacp on dangling member", 'findings/poc/DF-3344/fix_validation.log - patched kernel #1: ports=1 after destroy, LACP-OK/RR-OK, guest up']
PoC changes
no seed existed (new pass-2 finding); laggleak.c/udptx.c written from scratch. ifconfig syntax corrected during bring-up: DF uses 'laggport'/'laggproto', not FreeBSD 'addm'. SIOCGLAGG requires a preallocated port array passed via ra_port/ra_size (kernel recounts and clamps).
Verified recommended fix
Register/Deregister lagg_port_ifdetach on ifnet_detach_event (the event if_detach actually fires) - see fix.diff; optionally assert at registration that the event list is the live one.
Verdict
lagg_port_ifdetach is registered on ifnet_departure_event (if_lagg.c:218-220), an eventhandler key that nothing in the DragonFly kernel ever invokes (if_detach fires ifnet_detach_event, if.c:958); the only automatic port-removal path is therefore dead code. Destroying a member interface (e.g. ifconfig tap0 destroy frees the whole tap_softc with its embedded ifnet, if_tap.c:495) leaves a persistent dangling lp->lp_ifp in sc_ports. Demonstrated on the guest: (1) unprivileged SIOCGLAGG (any uid) keeps returning the destroyed member, reading its name from freed heap - after slab reuse the 'port name' mutated from 'tap0' to 'tap2', i.e. a live kernel-heap-reuse disclosure to unprivileged users; (2) unprivileged UDP tx through the dangling failover primary exercised freed-memory reads in lagg_link_active (survived - stale flags read inactive); (3) privileged 'ifconfig lagg0 laggproto lacp' drove lacp_port_create -> if_addmulti through the freed ifnet and panicked the kernel: Fatal trap 12, page fault at 0x3c0, Stopped at tapifioctl+0x158; guest down at DDB. Not a race - the UAF is permanent until the lagg is destroyed, and lagg destroy itself writes restores into the freed ifnet (if_lagg.c:763-766). Two-line fix (ifnet_detach_event) validated on a rebuilt kernel: member destroy now removes the port, the previously-fatal trigger succeeds, guest stays up.
No comments yet.