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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0366 Β· 7 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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 trigger
  • fix.diff β€” move LAGG_RLOCK above the early-exit, re-validate inside lock
  • VERDICT.md β€” full narrative
  • env.txt
VERDICT.md verdict lock-order violation confirmed; race not won in test
↓ download raw

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.

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 used
  • fix.diff β€” move the LAGG_RLOCK above the early-exit check
  • VERDICT.md β€” this file
  • manifest.json
  • env.txt

Fix verification

not_testable

compile 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).