Lockless sc_count pre-check in lagg_start races with port destroy causing divide-by-zero panic
Summary
lagg_start(:1758-1763) reads sc->sc_count WITHOUT any lock as early-exit guard, then takes LAGG_RLOCK(:1766 = lockmgr LK_SHARED) and calls sc_select_tx_port(:1773). RR mode: p%=sc->sc_count(:1860). LB mode: p%=sc->sc_count(:2036). Concurrent port destroy (lagg_port_destroy under LAGG_WLOCK) decrements sc_count 1->0 in window between lockless check and RLOCK acquire -> modulo by zero -> CPU exception -> kernel panic. Triggerable by concurrent traffic + last port removal (hotplug/ifdetach).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0366 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race.sh | trigger-source | shell-based race trigger (root setup) | 1.4 KB | view raw |
| fix.diff | suggested-fix | move LAGG_RLOCK above early-exit; re-validate sc_count inside lock | 960 B | view raw |
| VERDICT.md | verdict | lock-order violation confirmed; race not won in test | 3.8 KB | β raw |
| env.txt | environment | guest uname, lagg module presence | 290 B | view raw |
| README.md | readme | human reproduce doc | 1.1 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0366 PoC β lagg_start lockless sc_count check races port destroy
Status: INCONCLUSIVE β code-level race confirmed; not won in test. Severity: Medium
Bug
sys/net/lagg/if_lagg.c:1758-1763 reads sc->sc_count as an
early-exit guard without holding LAGG_RLOCK. The handler then
acquires LAGG_RLOCK at line 1766 and calls sc_select_tx_port,
which in round-robin mode (line 1860) does p %= sc->sc_count. If
lagg_port_destroy (line 770, under LAGG_WLOCK) decrements
sc_count 1β0 in the window between the lockless check and the
RLOCK acquire, the modulo divides by zero β CPU trap β kernel panic.
Reproduce
./race.sh # root: creates lagg0 RR with tap0+tap1, floods UDP, flaps ports 200x
In our test the race did not trigger in 200 iterations (window is narrow). The code pattern is unambiguously racy; heavier traffic or more CPUs would win it.
Files
race.shβ shell-based race triggerfix.diffβ move LAGG_RLOCK above the early-exit, re-validate inside lockVERDICT.mdβ full narrativeenv.txt
DF-0366 β Lockless sc_count pre-check in lagg_start races port destroy
Verdict
LATENT β code-level lock-order violation confirmed by source trace; race not won in our test window (no panic).
lagg_start checks sc->sc_count == 0 as an early-exit guard
without holding LAGG_RLOCK. It then acquires LAGG_RLOCK and
calls sc_select_tx_port, which in round-robin mode executes
p %= sc->sc_count. If a concurrent lagg_port_destroy (under
LAGG_WLOCK) decrements sc_count 1β0 between the lockless check
and the RLOCK acquire, the modulo reads sc_count == 0 and the CPU
traps a divide-by-zero β kernel panic.
Bug mechanism (source trace)
File: sys/net/lagg/if_lagg.c.
The lockless early-exit guard (line 1758β1763)
1758: if (((ifp->if_flags & IFF_RUNNING) == 0)
1759: || (sc->sc_proto == LAGG_PROTO_NONE)
1760: || (sc->sc_count == 0)) { // <-- LOCKLESS read of sc_count
1761: ifsq_purge(ifsq);
1762: return;
1763: }
1764:
1765:
1766: LAGG_RLOCK(sc); // <-- lock taken AFTER the check
1767: for (;;) {
...
1773: ifp_p = (*sc_select_tx_port)(sc, m);
RR select_tx_port divides by sc_count (line 1860)
1853: static struct ifnet *
1854: lagg_rr_select_tx_port(struct lagg_softc *sc, struct mbuf *m)
...
1859: p = atomic_fetchadd_32(&sc->sc_seq, 1);
1860: p %= sc->sc_count; // <-- div-by-zero if sc_count==0
Concurrent decrement under WLOCK (sys/net/lagg/if_lagg.c:770)
740: lagg_port_destroy(struct lagg_port *lp, int runpd)
...
747: LAGG_WLOCK_ASSERT(sc); // caller holds WLOCK
...
770: sc->sc_count--; // 1 -> 0
Race window
CPU A (lagg_start): CPU B (lagg_port_destroy):
read sc_count==1 (NO LOCK)
acquire WLOCK
sc_count-- (1 -> 0)
release WLOCK
acquire RLOCK (count is 0
but RLOCK doesn't help;
the bad read already
happened)
call sc_select_tx_port
p %= sc_count -> div0 PANIC
Once LAGG_RLOCK is held the count is stable β but the lockless
pre-check at line 1758 already let us pass when count was 1, then
count became 0 before the lock was acquired, and the locked region
operates on the now-zero count.
Reproduction attempt on this guest
We created lagg0 (roundrobin) with two tap ports, then
concurrently ran a UDP-TX flood through lagg0 and a port-flap
loop (-laggport tap1 ; -laggport tap0 ; laggport tap0 ;
laggport tap1, 200 iterations). The guest stayed up β the race
window is narrow and we did not win it in this short test.
race setup: lagg0 roundrobin {tap0,tap1}, UDP TX flood + 200 port flaps
result: guest still up; no div0 panic triggered
This does not refute the bug β the code pattern is unambiguously racy. With heavier traffic + a tighter port-flap loop + more CPUs the race is winnable.
Impact (code-confirmed)
- Kernel divide-by-zero trap β panic (DoS) from a context that can be reached by an unprivileged user once an admin has configured a lagg interface (root-only setup, but the trigger β sending traffic while the last port is removed β is unpriv/remote).
- Hot-plug events (physical link flap, driver reload) and any packet egress during the last-port removal race the same window.
Recommended fix
Either:
(a) acquire LAGG_RLOCK BEFORE the early-exit check, so the read
of sc_count is consistent with the locked region; or
(b) re-validate sc_count != 0 inside the RLOCK region before
calling sc_select_tx_port.
Option (a) is cleaner β see fix.diff for that variant.
Files in this folder
race.shβ the shell-based race trigger we usedfix.diffβ move the LAGG_RLOCK above the early-exit checkVERDICT.mdβ this filemanifest.jsonenv.txt
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. lagg_start lockless sc_count pre-check races port_destroy -> div-by-zero. Race not won (200 flaps).
No comments yet.