lagg_lacp_detach drops the lagg WLock mid-detach while sc_* protocol callbacks remain installed: ioctl re-entry into half-torn-down LACP state (incl. unprivileged SIOCGLAGG)
Summary
SIOCSLAGG calls sc->sc_detach(sc) and only afterwards NULLs the protocol callbacks. lagg_lacp_detach explicitly releases the lagg WLock for the duration of lacp_detach() and reacquires after; lacp_detach sets sc->sc_psc = NULL and kfrees the lacp_softc inside that window. Any thread taking the now-free lagg lock re-enters the half-torn-down protocol: unpriv SIOCGLAGG still sees sc_req == lacp_req and calls lacp_req() on NULL sc_psc -> NULL-deref; SIOCSLAGGPORT still sees sc_port_create == lacp_port_create and LIST_INSERT_HEADs into the freed lsc - UAF write; concurrent clone_destroy re-invokes the still-installed sc_detach -> double detach plus nested unlock. The 'Still detaching' EBUSY guard only covers a second SIOCSLAGG. NOT reproduced (6 unpriv laggspin processes vs 1800 proto flips on INVARIANTS guest - no panic; fatal sub-window real in source but too narrow at achievable ioctl rates, writer-priority lock queuing further limiting reader entry); structural defect stands. Fix: NULL every sc_* callback (incl. sc_select_tx_port, currently missing) BEFORE invoking a stashed-local detach pointer; snapshot+NULL sc_detach in clone_destroy.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3345 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| laggspin.c | β | 1.3 KB | view raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 176 B | view raw | |
| run.log | β | 732 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-3345 β lagg_lacp_detach drops the lagg WLock mid-detach while protocol callbacks remain installed
Summary
lagg_ioctl SIOCSLAGG (sys/net/lagg/if_lagg.c:1085-1101) calls
sc->sc_detach(sc) and only afterwards NULLs the protocol callbacks
(sc_req, sc_port_create, β¦, lines 1090-1100). The LACP implementation of
sc_detach β lagg_lacp_detach (sys/net/lagg/if_lagg.c:2091-2106) β
releases the lagg WLock at line 2101 (LAGG_WUNLOCK) for the duration of
lacp_detach() and reacquires it at 2103.
During that window the softc is in a half-torn-down state that any other thread can observe by simply taking the (now free) lagg lock:
- Unprivileged
SIOCGLAGG(ifconfig lagg0):lagg_ioctl:1055still seessc_req == lacp_req(it is NULLed only aftersc_detachreturns).lacp_req()(sys/net/lagg/ieee8023ad_lacp.c:606-607) doeslsc = LACP_SOFTC(sc)β andlacp_detach()setssc->sc_psc = NULLat ieee8023ad_lacp.c:848, inside the window β then dereferenceslsc->lsc_active_aggregatorβ NULL-deref panic. - Privileged
SIOCSLAGGPORTin the window:lagg_port_create:707still seessc_port_create == lacp_port_createβlacp_port_create()inserts into the freed/being-freedlsc(LIST_INSERT_HEAD(&lsc->lsc_ports, β¦)ieee8023ad_lacp.c:561) β UAF write into freed heap. - Privileged
ifconfig laggX destroyin the window:lagg_clone_destroy:427still seessc_detach != NULLβ invokeslagg_lacp_detacha second time β doublelacp_detach()on freed lsc, plus the nestedLAGG_WUNLOCKreleases the lock the destroyer believes it holds β lock-state corruption.
The else if (sc->sc_input != NULL) error = EBUSY guard (line 1101) only
protects a second SIOCSLAGG; it does nothing for SIOCGLAGG/SIOCSLAGGPORT/
clone-destroy, which re-enter through their own paths.
Verification attempt (2026-09-11, guest dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC)
- 3 then 6 unprivileged
laggspinprocesses (tight SIOCGLAGG loop), privileged flip looplaggproto lacp <-> failover, 1800 total flips. - No panic, guest stayed up. The fatal sub-window (between
sc->sc_psc = NULLat ieee8023ad_lacp.c:848 and the WLock reacquire at if_lagg.c:2103 β twocallout_drains +LACP_LOCK_DESTROY+kfree) is real but narrow, and writer-priority lock queuing further limits when a reader can slip in.
Verdict
Structural lock-protocol defect, certain at the source level (the unlock is
explicit and the callbacks demonstrably not yet NULLed); not manifested on
the guest within verification budget β not_reproduced.
Fix sketch
NULL every sc_* callback before invoking sc_detach (and stash
sc_detach in a local), so nothing can re-enter the protocol during the
detach window:
LAGG_WLOCK(sc);
if (sc->sc_proto != LAGG_PROTO_NONE) {
/* Reset protocol first in case detach unlocks */
sc->sc_proto = LAGG_PROTO_NONE;
- error = sc->sc_detach(sc);
+ int (*detach)(struct lagg_softc *) = sc->sc_detach;
sc->sc_detach = NULL;
sc->sc_start = NULL;
+ sc->sc_select_tx_port = NULL;
sc->sc_input = NULL;
sc->sc_port_create = NULL;
sc->sc_port_destroy = NULL;
sc->sc_linkstate = NULL;
sc->sc_init = NULL;
sc->sc_stop = NULL;
sc->sc_lladdr = NULL;
sc->sc_req = NULL;
sc->sc_portreq = NULL;
+ if (detach != NULL)
+ error = detach(sc);
}
(also make lagg_clone_destroy snapshot sc_detach into a local and NULL
the field before calling it, so a concurrent SIOCSLAGG cannot leave the
destroyer calling a protocol that SIOCSLAGG already detached).
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3345/run.log - 1800 flips x 6 spinners, vm.sh status up after both rounds', 'sys/net/lagg/if_lagg.c:2101-2103 (WUNLOCK window), 1090-1100 (callbacks NULLed only after detach returns)', 'sys/net/lagg/ieee8023ad_lacp.c:848 (sc_psc = NULL inside the window)']
PoC changes
no seed; laggspin.c written from scratch (tight SIOCGLAGG loop).
Verified recommended fix
NULL every sc_* protocol callback (including sc_select_tx_port) BEFORE invoking sc_detach, calling the stashed local pointer afterwards; snapshot+NULL sc_detach in lagg_clone_destroy before calling it.
Verdict
Structural lock-protocol defect, certain from source: SIOCSLAGG (if_lagg.c:1085-1101) NULLs the protocol callbacks only AFTER sc->sc_detach() returns, but lagg_lacp_detach (if_lagg.c:2091-2106) explicitly drops the lagg WLock at 2101 for the duration of lacp_detach(), which NULLs sc->sc_psc (ieee8023ad_lacp.c:848) and kfrees the lacp_softc. During that window an unprivileged SIOCGLAGG still sees sc_req == lacp_req and calls lacp_req() on sc_psc == NULL (deref of lsc->lsc_active_aggregator, ieee8023ad_lacp.c:607); a concurrent SIOCSLAGGPORT still sees sc_port_create == lacp_port_create and LIST_INSERT_HEADs into the freed lsc (ieee8023ad_lacp.c:561); a concurrent clone-destroy re-invokes the still-installed sc_detach (double lacp_detach + nested WUNLOCK). Verification attempt: 6 unprivileged SIOCGLAGG spinner processes vs 1800 privileged laggproto lacp<->failover flips on the stock INVARIANTS guest - no panic, guest stayed up; the fatal sub-window (sc_psc NULLed until WLock reacquire) is real but too narrow to manifest at achievable ioctl rates. Filed as not reproduced / likely; the fix (NULL all callbacks before calling detach, and snapshot sc_detach in clone_destroy) is cheap and also hardens DF-3346.
No comments yet.