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

jme_rxeof() RX walk has no work bound on the interrupt path (count=-1 at all three call sites): a remote line-rate small-packet flood keeps the ring perpetually non-empty so the interrupt handler loops forever holding the serializer β€” NIC + CPU livelock, whole-interface wedge on legacy/MSI (DF-3761/alc family, jme(4) instance)

Summary

jme_rxeof's only work bound is the count parameter and every interrupt-context call site passes -1 (jme_rx_intr, jme_msix_rx, jme_rx_restart). Each iteration is us-scale vs the 672ns 64-byte 1GbE line-rate inter-arrival budget, so a sustained remote small-packet flood keeps the ring perpetually non-empty and the handler never returns while holding its serializer: IRQ-thread CPU spin + ring (MSI-X) or whole-interface (legacy/MSI, including jme_stop/jme_init - recovery via ifconfig down is itself blocked) livelock. The bound provably exists and is used by the driver's own ifpoll path (jme_npoll_rx passes cycle); the driver has no bound knob at all. DFly msk(4) bounds its own walk; FreeBSD bounds these loops. Remote, unauthenticated, default configuration: flood jme0 with minimum-size frames at line rate. Verified at source level on the guest (the five call sites with arguments + zero process_limit references). Fix: tunable work budget at the three interrupt-context call sites (handlers mask their interrupt on entry and re-enable at exit, so a bounded early exit re-interrupts immediately - no frame loss).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3778 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 5.2 KB ↓ raw
trigger.c β€” 4.0 KB view raw
run.log β€” 466 B view raw
fix_build.log β€” 616.0 KB ↓ download
env.txt β€” 448 B view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 4.7 KB view raw

DF-3778 β€” jme_rxeof() RX walk has no work bound on the interrupt path: remote line-rate flood keeps the ring perpetually non-empty, handler spins holding the serializer (NIC + CPU livelock)

File: sys/dev/netif/jme/if_jme.c (pass 2) Class: DF-3761 (alc) family β€” independent jme(4) instance Severity: Medium Β· CWE: CWE-834 (Uncontrolled Recursion/Consumption) Β· Confidence: certain (source), runtime hardware-gated

What

jme_rxeof (:2488-2562) has exactly one work bound β€” the count parameter (:2496) β€” and every interrupt-context call site passes -1: jme_rx_intr :3500 (legacy/MSI jme_intr), jme_msix_rx :3845, jme_rx_restart :3882 (jme_msix_status/jme_npoll_status). Only the ifpoll path bounds it (:3341, cycle). Each iteration is Β΅s-scale (jme_newbuf = m_getcl + bus_dmamap_load_mbuf_segment :3061-3074, plus if_input dispatch), while a 64-byte flood on 1GbE arrives every 672ns β†’ under sustained small-packet line-rate the ring never drains and the handler never returns:

  • MSI-X RX: the ring's serializer is held for the whole walk (:3837) β€” that CPU spins, all other ring entry points block;
  • legacy/MSI: jme_intr takes the MAIN serializer (:3912 registration, :2118 assert) then walks every ring (:3499) β€” the whole interface wedges behind an unbounded walk, plus the interrupt thread pins its CPU.

No bound knob exists at all in the driver (process_limit: 0 hits, run.log) β€” the count parameter and the ifpoll call site prove the bound was intended and available. Sibling precedent: DF-3761 (alc, knob dead), DFly msk(4) bounds its own walk; FreeBSD bounds these loops.

Trigger

Remote, unauthenticated, default configuration: flood jme0 with minimum-size frames at line rate while the interface is up.

Reproduce

Hardware-gated on this guest (no JMicron NIC β€” see run.log presence probe; if_jme is compiled into the running kernel). Source-level argument is complete in trigger.c; per family convention (DF-3761) the verdict is inconclusive with impact none-on-this-guest.

Fix

fix.diff β€” hw.jme.rx_proc_limit (default 64, tunable) passed at the three interrupt-context call sites; safe because all three handlers mask their interrupt on entry and re-enable at exit, so a bounded early exit re-interrupts immediately. The :418 link-reconfig drain stays unbounded (interrupts masked, must fully drain). Compile-validated (fix_build.log: nativekernel RC=0, 0 warnings).

VERDICT.md
↓ download raw

DF-3778 β€” VERDICT

Status: source-level certain (loop provably unbounded on the interrupt path; bound provably available and used elsewhere in the same function); runtime honestly inconclusive β€” the trigger is a remote line-rate flood against JMC250/JMC260 hardware the audit guest does not have. reproduced=0, impact=none-on-this-guest; class ceiling on real hardware: remote-triggerable NIC + single-CPU (legacy/MSI: whole-interface) livelock DoS on default config. DF-3761 (alc) family, independent if_jme.c instance.

Root cause (path:line)

  1. sys/dev/netif/jme/if_jme.c:2494-2497 β€” the RX walk's only work bound is the count parameter: if (count >= 0 && count-- == 0) break;. With count == -1 the loop exits only when the ring drains (OWN set, :2500) or a consistency break fires (:2502/:2513/:2551).
  2. Interrupt-context call sites all pass -1: * jme_rx_intr β€” sys/dev/netif/jme/if_jme.c:3500 (from jme_intr :2150; handler registered on the MAIN serializer sc->jme_serialize, :3912, asserted at :2118 β€” the whole interface is behind the walk; jme_intr masks all interrupts at :2125 and re-enables at :2174); * jme_msix_rx β€” :3845 (handler registered on the ring serializer, :3672; masks jme_rx_coal at :3839, unmasks at :3847); * jme_rx_restart β€” :3882 (from jme_msix_status :3866 / jme_npoll_status :3330; INTR_RXQ_DESC_EMPTY masked at entry).
  3. Per-iteration cost: jme_rxpkt β†’ jme_newbuf (m_getcl + bus_dmamap_load_mbuf_segment, :3061-3082) + if_input dispatch (:2474) β€” Β΅s-scale, i.e. slower than 1.488 Mpps 64-byte line-rate arrival (672ns budget) on the 1GbE JMC250, so a sustained small-packet flood keeps the ring perpetually non-empty and the walk never terminates while the handler holds its serializer.
  4. The bound is provably available and intended: the same function is called WITH a bound by the driver's own ifpoll path (jme_npoll_rx, :3341, passes cycle); DragonFly's msk(4) bounds its own walk; the alc(4) twin of this defect is filed as DF-3761; FreeBSD bounds these loops. jme has no bound knob at all (grep -c process_limit if_jme.c == 0, run.log).

Impact analysis (on hardware)

Remote, unauthenticated, default configuration (interface up): flood at line rate β†’

  • MSI-X RX vector: that ring's jme_rx_serialize is held forever β†’ the IRQ thread spins at 100% on its CPU; every other entry point on the ring (ioctls, npoll, RX restart) blocks indefinitely;
  • legacy/MSI interrupt: sc->jme_serialize (main) held β†’ the whole interface (including jme_stop/jme_init, i.e. recovery via ifconfig jme0 down) blocks β€” the livelock cannot be administratively cleared without a reboot on legacy/MSI configurations;
  • lower-priority kernel threads on the pinned CPU starve.

Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)

run.log (2026-09-16): presence probe β€” no JMicron PCI device (vtnet0 lo0 only); driver compiled into the running kernel (kldstat -v: 102 jme/miibus, 103 pci/if_jme); guest-side grep of /usr/src β€” the five jme_rxeof(rdata, ...) call sites with their arguments (three Γ— -1 interrupt-path, one Γ— -1 drain-path, one Γ— cycle bounded ifpoll) and zero process_limit references. Runtime reproduction requires a JMicron NIC under line-rate flood β†’ hardware- gated, inconclusive per family convention (DF-3761 precedent).

Fix validation

  • fix.diff (git-apply clean): introduces jme_rx_proc_limit (default 64, TUNABLE_INT("hw.jme.rx_proc_limit", ...)) and passes it at the three interrupt-context call sites (:3500/:3845/:3882).
  • Safety argument: each of those handlers masks its interrupt on entry and re-enables it at exit (jme_msix_rx :3839/:3847, jme_intr :2125/:2174, jme_msix_status :3859/:3869), so a bounded early exit with work remaining raises a fresh interrupt immediately β€” no frame loss, only the unbounded spin is removed. The jme_miibus_statchg drain (:418) is deliberately left unbounded: it runs at link reconfiguration with all interrupts masked (:403) and must fully drain the ring before the indices are reset (:427).
  • Compile-validated on the guest: all three jme fixes (DF-3776/3777/ 3778) applied together to /usr/src, incremental nativekernel X86_64_GENERIC build β†’ RC=0, zero warnings (fix_build.log).
  • Driver-level runtime validation impossible without JMicron hardware β†’ fix_status: not_testable.

Bottom line

jme(4)'s interrupt-path RX walk is unbounded (count=-1 at all three interrupt call sites) in a driver whose ifpoll path proves the bound exists; a remote line-rate small-packet flood livelocks the handler holding the ring serializer (legacy/MSI: the main serializer β€” the whole interface, un-recoverable by ifconfig). The DF-3761/alc family defect, independently instantiated in if_jme.c; fix is a 64-frame budget knob at three call sites, compile-clean.

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff (git-apply clean) adds a jme_rx_proc_limit work budget (default 64, tunable) at the three interrupt-context call sites (jme_rx_intr :3500, jme_msix_rx :3845, jme_rx_restart :3882), mirroring the bound the driver's own ifpoll path already applies (:3341). Safe: each of those handlers masks its interrupt on entry and re-enables at exit, so a bounded early exit with work remaining raises a fresh interrupt immediately (no frame loss). The jme_miibus_statchg drain (:418) intentionally stays unbounded (interrupts masked, must fully drain before index reset). Compile validated: nativekernel build with all three jme fixes -> RC=0, zero warnings (fix_build.log). Driver-level runtime validation impossible without JMicron hardware.

fix.diff hunks applied to /usr/src (guest): grep shows jme_rx_proc_limit at :253/:255 and passed at :3535/:3880/:3917; fix_build.log: '>>> Kernel build for X86_64_GENERIC completed' RC=0, grep -cE 'error:|warning:' == 0.
↓ fix.diffper-fix-DF-3778

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: guest-side presence probe (vtnet0 lo0 only, NO_JMICRON_PCI, kldstat -v shows 102 jme/miibus + 103 pci/if_jme) plus the grep of /usr/src/sys/dev/netif/jme/if_jme.c listing all five jme_rxeof(rdata,...) call sites with their arguments (:418 -1 drain, :3341 cycle BOUNDED, :3500 -1, :3845 -1, :3882 -1) and 'process_limit' count 0; trigger.c: fully annotated source excerpt with the per-iteration cost and mask/unmask-restart safety argument; fix_build.log: nativekernel RC=0 with fixes applied.

PoC changes

n/a (source-level finding, hardware-gated runtime; trigger.c is an annotated source-excerpt analysis, following the DF-3761 pack convention)

Verified recommended fix

Bound the three interrupt-context jme_rxeof call sites with a tunable rx work budget (hw.jme.rx_proc_limit, default 64), mirroring the ifpoll path's existing count bound (see fix.diff).

Verdict

Source-level certain, runtime honestly inconclusive (hardware-gated). jme_rxeof's RX walk (sys/dev/netif/jme/if_jme.c:2494-2562) has exactly one work bound - the count parameter (:2496) - and every interrupt-context call site passes -1: jme_rx_intr (:3500, from jme_intr :2150, registered on the MAIN serializer :3912), jme_msix_rx (:3845, ring serializer :3672), jme_rx_restart (:3882). Each iteration is us-scale (jme_newbuf m_getcl + bus_dmamap_load_mbuf_segment :3061-3082, if_input :2474) vs a 672ns 64-byte 1GbE line-rate inter-arrival budget, so a sustained remote small-packet flood keeps the ring perpetually non-empty and the interrupt handler never returns while holding its serializer: IRQ-thread CPU spin + ring (MSI-X) or whole-interface (legacy/MSI, incl. jme_stop/jme_init recovery) livelock. The bound provably exists and is used by the driver's own ifpoll path (jme_npoll_rx :3341 passes cycle); the driver has no bound knob at all (process_limit: 0 hits, run.log). This is the DF-3761 (alc) family defect independently instantiated in if_jme.c; DragonFly msk(4) bounds its own walk and FreeBSD bounds these loops. Guest has no JMicron NIC (run.log presence probe; if_jme compiled into the running kernel) -> inconclusive per family convention, impact none-on-this-guest.