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

xl_rxeof() interrupt-mode Rx walk has no work bound β€” the count>=0 budget exists but every interrupt-path caller passes -1, and non-905B chips goto-again restart the walk on uplist stall (DF-3761 family)

Summary

xl_rxeof only honors a budget when count >= 0 (ifpoll-only); xl_intr passes -1, the resync retry loop is an unbounded while over resync + rxeof(-1), start/watchdog pass -1, and for boomerang chips the stall/reload/goto-again re-runs the whole walk under exactly the line-rate-flood state. Remote-unauthenticated RX livelock holding ifp->if_serializer inside xl_intr, starving softclock/netisr (and enabling the DF-3906 wedge window). The ifpoll path proves the bound plumbing works when given a real count. Pass-the-budget fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3907 Β· 4 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
fix.diff β€” 1.5 KB view raw
manifest.json β€” 993 B view raw
verdict.json β€” 3.0 KB view raw

DF-3907 β€” xl_rxeof() interrupt-mode Rx walk has no work bound (DF-3761 family, xl(4) instance)

File: sys/dev/netif/xl/if_xl.c (pass 2, GLM 5.3) Hardware-gated: the audit guest has no 3Com 3c90x NIC; source-level certainty; status inconclusive per family convention (same treatment as DF-3761/DF-3904).

The bug (source)

xl_rxeof(struct xl_softc *sc, int count) (sys/dev/netif/xl/if_xl.c:1948) already has the plumbing for a work bound β€” but only the ifpoll(4) path uses it:

while ((rxstat = le32toh(sc->xl_cdata.xl_rx_head->xl_ptr->xl_status))) {
#ifdef IFPOLL_ENABLE
        if (count >= 0 && count-- == 0)     /* :1960 - bound ONLY when count >= 0 */
                break;
#endif
        ...
}

Every interrupt-path caller passes -1, disabling the bound entirely:

  • xl_intr() :2335 β€” xl_rxeof(sc, -1) (main RX path)
  • xl_intr() :2340 β€” while (xl_rx_resync(sc)) xl_rxeof(sc, -1); (unbounded resync retry loop)
  • xl_start_body() :2616 β€” xl_rxeof(sc, -1) (TX-completion RX assist)
  • xl_watchdog() :3123 β€” xl_rxeof(sc, -1)

Additionally, for non-905B (boomerang) chips the function ends with a stall/reload/goto again (:2041-2063) that restarts the whole unbounded walk whenever the upload engine hit the end of the ring β€” precisely the state a line-rate flood produces.

The walk runs inside xl_intr() holding ifp->if_serializer (bus_setup_intr(..., ifp->if_serializer) at :1529-1531).

Consequence (on xl hardware): a remote unauthenticated line-rate flood keeps xl_status non-zero forever β†’ the interrupt handler never returns, pinning its CPU and the serializer (RX livelock / remote DoS; DF-3761 family, alc(4) twin).

Why not runtime-verified

No 3c90x NIC on the audit QEMU guest. The bound plumbing exists and is proven correct in the ifpoll path (xl_npoll_compat passes a real count, :2273); the finding is that the default interrupt mode never uses it. verdict.json records inconclusive, reproduced=0.

Fix

fix.diff: pass a XL_RX_PROC_MAX (64) budget from the interrupt-path callers (the count plumbing already implements the bound β€” count >= 0 enables it), and cap the while (xl_rx_resync(...)) retry loop. The ifpoll path is unchanged.

Fix verification

inconclusive
baseline no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree: enables the function's existing count>=0 bound from the interrupt path. Not runnable on this guest (no 3c90x NIC); the same bound is proven live by the ifpoll path in-tree.

findings/poc/DF-3907/fix.diff
↓ fix.diffper-fix-DF-3907

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3907/README.md (all four count=-1 call sites + goto-again analysis); findings/poc/DF-3907/fix.diff (budget via the existing count plumbing)

PoC changes

No seed PoC; no runnable trigger exists without 3c90x hardware. The defect is the count=-1 constant at four call sites against a bound that the same function already implements for ifpoll.

Verified recommended fix

Pass XL_RX_PROC_MAX=64 (instead of -1) from the interrupt-path xl_rxeof call sites and cap the xl_rx_resync retry loop; the count>=0 bound already exists in xl_rxeof.

Verdict

Source-certain, hardware-gated (no 3Com 3c90x NIC on the audit guest) -> inconclusive per family convention, same treatment as DF-3761 (alc) and DF-3904 (sk). xl_rxeof (sys/dev/netif/xl/if_xl.c:1948) HAS a work-bound parameter but it is only honored when count >= 0 (:1959-1962) and every interrupt-path caller passes -1: xl_intr :2335 (main RX), xl_intr :2340 (the unbounded while(xl_rx_resync(sc)) xl_rxeof(sc,-1) retry loop), xl_start_body :2616 (TX-completion RX assist), xl_watchdog :3123; for non-905B chips a goto-again restart (:2041-2063) further re-runs the walk whenever the uplist stalled - the exact state a line-rate flood produces. The walk executes inside xl_intr holding ifp->if_serializer (bus_setup_intr at :1529-1531), so a remote unauthenticated flood pins the interrupt CPU and serializer indefinitely (RX livelock / remote DoS). The ifpoll path (xl_npoll_compat :2273) proves the bound plumbing works when given a real count. fix.diff passes XL_RX_PROC_MAX=64 from the interrupt-path callers and caps the resync retry loop; ifpoll behavior unchanged.