β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3345

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3345 Β· 7 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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:1055 still sees sc_req == lacp_req (it is NULLed only after sc_detach returns). lacp_req() (sys/net/lagg/ieee8023ad_lacp.c:606-607) does lsc = LACP_SOFTC(sc) β€” and lacp_detach() sets sc->sc_psc = NULL at ieee8023ad_lacp.c:848, inside the window β€” then dereferences lsc->lsc_active_aggregator β†’ NULL-deref panic.
  • Privileged SIOCSLAGGPORT in the window: lagg_port_create:707 still sees sc_port_create == lacp_port_create β†’ lacp_port_create() inserts into the freed/being-freed lsc (LIST_INSERT_HEAD(&lsc->lsc_ports, …) ieee8023ad_lacp.c:561) β†’ UAF write into freed heap.
  • Privileged ifconfig laggX destroy in the window: lagg_clone_destroy:427 still sees sc_detach != NULL β†’ invokes lagg_lacp_detach a second time β†’ double lacp_detach() on freed lsc, plus the nested LAGG_WUNLOCK releases 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 laggspin processes (tight SIOCGLAGG loop), privileged flip loop laggproto lacp <-> failover, 1800 total flips.
  • No panic, guest stayed up. The fatal sub-window (between sc->sc_psc = NULL at ieee8023ad_lacp.c:848 and the WLock reacquire at if_lagg.c:2103 β€” two callout_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_testable
per-fix-DF-3345

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