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)
PoC verification
Evidence pack
findings/poc/DF-3778 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_intrtakes 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).
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)
sys/dev/netif/jme/if_jme.c:2494-2497β the RX walk's only work bound is thecountparameter:if (count >= 0 && count-- == 0) break;. Withcount == -1the loop exits only when the ring drains (OWN set, :2500) or a consistency break fires (:2502/:2513/:2551).- Interrupt-context call sites all pass -1:
*
jme_rx_intrβsys/dev/netif/jme/if_jme.c:3500(fromjme_intr:2150; handler registered on the MAIN serializersc->jme_serialize, :3912, asserted at :2118 β the whole interface is behind the walk;jme_intrmasks all interrupts at :2125 and re-enables at :2174); *jme_msix_rxβ:3845(handler registered on the ring serializer, :3672; masksjme_rx_coalat :3839, unmasks at :3847); *jme_rx_restartβ:3882(fromjme_msix_status:3866 /jme_npoll_status:3330; INTR_RXQ_DESC_EMPTY masked at entry). - Per-iteration cost:
jme_rxpktβjme_newbuf(m_getcl+bus_dmamap_load_mbuf_segment, :3061-3082) +if_inputdispatch (: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. - 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, passescycle); 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_serializeis 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 (includingjme_stop/jme_init, i.e. recovery viaifconfig 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): introducesjme_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_statchgdrain (: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
nativekernelX86_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_testablefix.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.
Confirmed kernel references
- sys/dev/netif/jme/if_jme.c:2494
- sys/dev/netif/jme/if_jme.c:2496
- sys/dev/netif/jme/if_jme.c:3341
- sys/dev/netif/jme/if_jme.c:3500
- sys/dev/netif/jme/if_jme.c:3845
- sys/dev/netif/jme/if_jme.c:3882
- sys/dev/netif/jme/if_jme.c:2150
- sys/dev/netif/jme/if_jme.c:3912
- sys/dev/netif/jme/if_jme.c:3672
- sys/dev/netif/jme/if_jme.c:3061
- sys/dev/netif/jme/if_jme.c:2474
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.
No comments yet.