jme_rxeof() descriptor consistency check admits (nsegs=0, pktlen=0): jme_rxpkt neither reposts the descriptor nor advances jme_rx_cons β unbounded in-handler spin (RX halt + serializer-held CPU softlock) on faulty/malicious controller writeback; the DF-3604-family heap-OOB is otherwise CLOSED by the :2513 cross-check
Summary
The sole validation of the controller-DMA'd RX descriptor is nsegs != howmany(pktlen, MCLBYTES). The pair (0,0) passes because howmany(0,2048)==0. With nsegs==0, jme_rxpkt runs neither the repost loop nor jme_discard_rxbufs and advances jme_rx_cons by 0 - so the OWN-clear VALID-set descriptor is never re-armed and jme_rxeof's for(;;) (count=-1) re-reads it forever: the RX interrupt thread spins holding the ring serializer, RX halts, CPU softlocks. Requires a faulty/malicious controller (the DF-3604-family threat model). VERIFIED on the guest via a verbatim-loop replica in a KLD: 'guard=-1 iterations=200000000 reposts=0 rx_cons 0 -> 0' (the 200M guard was the ONLY thing that ended the walk - shipped code has none); fixed shape terminates at 2 iterations. Positive result: the same cross-check closes the DF-3604-family heap-OOB for jme.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3777 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| jme_callout_replica.c | β | 14.8 KB | view raw | |
| Makefile | β | 139 B | β download | |
| build.log | β | 63 B | view raw | |
| run.log | β | 439 B | view raw | |
| fix_build.log | β | 616.0 KB | β download | |
| env.txt | β | 448 B | view raw | |
| fix.diff | β | 844 B | view raw | |
| manifest.json | β | 2.0 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-3777 β jme_rxeof() admits (nsegs=0, pktlen=0): descriptor never re-armed, unbounded handler spin (faulty-device robustness hole; DF-3604-family OOB is otherwise CLOSED by the :2513 cross-check)
File: sys/dev/netif/jme/if_jme.c (pass 2)
Severity: Low Β· CWE: CWE-670 / CWE-20 Β· Confidence: certain (loop math, demonstrated on guest via verbatim-loop replica)
What
jme_rxeof (if_jme.c:2494-2562) validates the controller-DMA'd RX
descriptor with a single cross-check:
nsegs = JME_RX_NSEGS(le32toh(desc->buflen)); /* 7-bit :2511 */
pktlen = JME_RX_BYTES(le32toh(desc->buflen)); /* 16-bit :2512 */
if (nsegs != howmany(pktlen, MCLBYTES)) /* :2513 */
break;
This closes the DF-3604/3639/3663/3679/3713/3759 OOB-read family for
jme (pktlen is bound to nsegsΓ2048 posted clusters) β but the pair
(nsegs=0, pktlen=0) passes it (howmany(0,2048)==0). With nsegs==0,
jme_rxpkt (:2304-2486) executes neither the jme_newbuf() repost loop
(:2361) nor jme_discard_rxbufs() (:2350), and advances jme_rx_cons
by 0 (:2484-2485) β so the OWN-clear, VALID-set descriptor is never
re-armed and jme_rxeof's for(;;) re-reads the same descriptor
forever: the MSI-X RX interrupt thread (or legacy handler / netisr
poll) spins holding the ring serializer; RX halts; that CPU softlocks.
Sub-case (benign, noted for completeness): pktlen β [1,9] yields a
negative jme_rxlen (:2360) and negative m_len/m_pkthdr.len
(:2403/:2412) β ether_input drops it (m->m_len < sizeof(struct
ether_header), sys/net/if_ethersubr.c:1501), so no memory-safety issue.
Threat model
Compliant JMicron silicon always writes nsegs β₯ 1 for a completed frame; triggering requires a faulty or malicious controller (misbehaving DMA writes β the same threat model as the DF-3604 family findings, e.g. Thunderbolt/evil-peripheral or buggy silicon). No unprivileged-software or remote-packet trigger exists: wire traffic cannot make the NIC report a zero-segment completed descriptor.
Reproduce (loop-math replica, no hardware needed)
The jme_callout_replica.ko module (shared with DF-3776) contains the
verbatim jme_rxeof loop logic against a simulated ring:
vm.sh run_root 'kldload /root/kmod/jme_callout_replica.ko; sysctl kern.jmereplica.rxspin=1; \
sysctl kern.jmereplica.rxspin_fix=1; dmesg | grep rxspin | tail -2'
Expected (observed, run.log):
* broken shape: iterations=200000000 reposts=0 rx_cons 0 -> 0 β the
full guard consumed, zero reposts, consumer unchanged β unbounded;
* fix shape (fix.diff): iterations=2 reposts=1 rx_cons 0 -> 1 β
terminates, degenerate descriptor reposted.
Fix
fix.diff β reject nsegs < 1 before the consistency check: repost the
one degenerate descriptor (jme_discard_rxbufs(..., 1)) and advance
(JME_DESC_INC), continue. Compile-validated (fix_build.log).
DF-3777 β VERDICT
Status: source-level certain (loop math) and demonstrated on the guest kernel by a verbatim-loop replica (200M-iteration guard fully consumed, zero reposts, consumer unchanged); jme-specific runtime hardware-gated (no JMicron NIC on the guest, and the trigger additionally requires a faulty/malicious controller) β inconclusive per family convention, reproduced=0, impact=none-on-this-guest. Class of impact on faulty hardware: unbounded RX interrupt-handler spin holding the ring serializer (interface RX halt + one CPU softlock).
Root cause (path:line)
sys/dev/netif/jme/if_jme.c:2511-2518β the only validation of the controller-DMA'd RX descriptor isnsegs != howmany(pktlen, MCLBYTES).JME_RX_NSEGSis a 7-bit field (sys/dev/netif/jme/if_jmereg.h:985-986) andJME_RX_BYTESa 16-bit field (:995). The pair (0, 0) passes the check becausehowmany(0, 2048) == 0.sys/dev/netif/jme/if_jme.c:2304-2486βjme_rxpkt()with nsegs == 0: the segment loopfor (count = 0; count < nsegs; count++)(:2361) never runs β neitherjme_newbuf()norjme_discard_rxbufs()reposts the descriptor; the error path is not taken either (JME_RX_ERR_STAT clear).rdata->jme_rx_cons += nsegs(:2484) advances by 0.- β the enclosing
for (;;)injme_rxeof(:2494) re-reads the same OWN-clear (:2500), VALID-set (:2502) descriptor forever. Callers pass count = -1 (unbounded) fromjme_msix_rx(:3845),jme_rx_intr(:3500) andjme_rx_restart(:3882): the interrupt thread spins in-kernel holdingrdata->jme_rx_serialize, RX halts, and with legacy/MSI interruptssc->jme_serialize(main) is also held for the whole spin. - Also note the positive result of the pass-2 RX checklist: the
:2513 cross-check does close the DF-3604/3639/3663/3679/3713/3759
heap-OOB-read family for jme β pktlen is bound to nsegs β€ 32 posted
2048-byte clusters (pktlen β€ 65535 β howmany β€ 32), and the chain
trimming at :2403-2413 keeps every m_len within its cluster. Only
the degenerate (0,0) pair and the (benign) pktlen<10 negative-length
edge survive: the negative m_len is dropped by
ether_input(m->m_len < sizeof(struct ether_header), sys/net/if_ethersubr.c:1501) β defensive depth only, no memory unsafety.
Threat model
Compliant JMicron silicon always reports nsegs β₯ 1 for a completed frame (and runt/error frames carry JME_RX_ERR_STAT β discarded at :2348-2357). The trigger requires a faulty or malicious controller writing crafted descriptor state via DMA β the same threat model the audit applied to the DF-3604 family. No unprivileged-local or remote software trigger exists.
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)
Presence probe (run.log): no JMicron PCI device (vtnet0 lo0 only);
driver compiled into the running kernel (kldstat -v: 103 pci/if_jme)
β runtime hardware-gated.
Loop math, demonstrated by jme_callout_replica.ko (the module shared
with DF-3776; repl_rxeof_spin() is the verbatim :2494-2562 loop logic
against a simulated 16-descriptor ring whose desc[0] is OWN-clear,
VALID-set, nsegs=0, pktlen=0):
sysctl kern.jmereplica.rxspin=1(broken shape):jmereplica[rxspin]: guard=-1 iterations=200000000 reposts=0 rx_cons 0 -> 0β the entire 200,000,000-iteration guard was consumed (the guard is the ONLY thing that ended the loop; the real code has none), zero reposts performed, consumer pointer unchanged β i.e. the descriptor is provably never re-armed and the loop provably never terminates.sysctl kern.jmereplica.rxspin_fix=1(fix.diff shape):jmereplica[rxspin_fix]: iterations=2 reposts=1 rx_cons 0 -> 1 (terminates; degenerate desc reposted)β the fixed logic exits after reposting and advancing past the degenerate descriptor.
Fix validation
fix.diff(git-apply clean):if (nsegs < 1)before the howmany check β repost the single degenerate descriptor (jme_discard_rxbufs(rdata, rdata->jme_rx_cons, 1)) and advance (JME_DESC_INC), thencontinue.- Mechanism validated live by the
rxspin_fixreplica control (terminates in 2 iterations, 1 repost). - Compile-validated on the guest: all three jme fixes (DF-3776/3777/3778)
applied together, incremental
nativekernelX86_64_GENERIC build β RC=0, zero warnings (fix_build.log). - Driver-level runtime validation impossible without (faulty) hardware β fix_status: not_testable.
Bottom line
Robustness hole in the RX descriptor validation: the consistency check admits (nsegs=0, pktlen=0), which makes the handler neither repost nor advance past the descriptor β an unbounded in-handler spin on faulty hardware. The surrounding length handling is otherwise sound (the DF-3604 family OOB does not apply to jme). One-line-class fix, compile-clean, mechanism-proven on the guest.
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 an explicit 'nsegs < 1' rejection that reposts the degenerate descriptor and advances past it before the howmany() consistency check. Mechanism validated live: replica control with the fixed shape terminates in 2 iterations with 1 repost and rx_cons advanced (vs 200M+ iterations / 0 reposts / unchanged consumer in the shipped shape). Compile validated: nativekernel build with all three jme fixes applied -> RC=0, zero warnings (fix_build.log). Driver-level runtime validation impossible without faulty JMicron hardware.
run.log: 'jmereplica[rxspin_fix]: iterations=2 reposts=1 rx_cons 0 -> 1 (terminates; degenerate desc reposted)'; 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:2500
- sys/dev/netif/jme/if_jme.c:2511
- sys/dev/netif/jme/if_jme.c:2513
- sys/dev/netif/jme/if_jme.c:2350
- sys/dev/netif/jme/if_jme.c:2361
- sys/dev/netif/jme/if_jme.c:2484
- sys/dev/netif/jme/if_jme.c:3845
- sys/dev/netif/jme/if_jme.c:3500
- sys/dev/netif/jme/if_jmereg.h:985
- sys/dev/netif/jme/if_jmereg.h:995
- sys/net/if_ethersubr.c:1501
Detail
Evidence (decisive lines)
run.log: 'jmereplica[rxspin]: guard=-1 iterations=200000000 reposts=0 rx_cons 0 -> 0 (unchanged == descriptor never re-armed: loop provably unbounded)' and the fix-shape control 'jmereplica[rxspin_fix]: iterations=2 reposts=1 rx_cons 0 -> 1 (terminates; degenerate desc reposted)'; jme_callout_replica.c repl_rxeof_spin()/repl_rxpkt() are the verbatim if_jme.c:2494-2562/:2304-2486 loop logic against a simulated ring; fix_build.log: nativekernel RC=0 with fixes applied.
PoC changes
n/a (source-level finding; verbatim-loop replica written for this pack, sharing the module with DF-3776)
Verified recommended fix
jme_rxeof: reject nsegs < 1 before the howmany() check - repost the single degenerate descriptor (jme_discard_rxbufs(...,1)), advance jme_rx_cons (JME_DESC_INC), continue (see fix.diff).
Verdict
Source-level certain and demonstrated on the guest kernel by a verbatim-loop replica (jme_callout_replica.ko: sysctl kern.jmereplica.rxspin=1 -> 'iterations=200000000 reposts=0 rx_cons 0 -> 0', i.e. the 200M-iteration guard was the ONLY thing that ended the walk; zero reposts; consumer unchanged -> descriptor provably never re-armed, loop provably unbounded). jme-specific runtime hardware-gated: the guest has no JMicron NIC, and the trigger additionally requires a faulty/malicious controller (compliant silicon always reports nsegs>=1; error frames carry JME_RX_ERR_STAT and are discarded at :2348) -> inconclusive per family convention. Root cause: the sole validation of the DMA'd RX descriptor, 'nsegs != howmany(pktlen, MCLBYTES)' (if_jme.c:2513), admits the pair (nsegs=0, pktlen=0) because howmany(0,2048)==0; jme_rxpkt (:2304-2486) with nsegs==0 executes neither the jme_newbuf repost loop (:2361) nor jme_discard_rxbufs (:2350) and advances jme_rx_cons by 0 (:2484), so jme_rxeof's unbounded for(;;) (:2494, count=-1 from jme_msix_rx :3845 / jme_rx_intr :3500 / jme_rx_restart :3882) re-reads the same OWN-clear VALID-set descriptor forever: RX interrupt thread spins holding the ring serializer, RX halts, CPU softlock. Positive pass-2 note: the :2513 cross-check CLOSES the DF-3604/3639/3663/3679/3713/3759 heap-OOB-read family for jme (pktlen bound to nsegs<=32 posted 2048-byte clusters; chain trim :2403-2413 keeps m_len in-cluster), and the pktlen<10 negative-m_len edge is dropped benignly by ether_input (if_ethersubr.c:1501).
No comments yet.