rl(4): any RX-error frame triggers a full adapter re-init in interrupt context under the ifnet serializer (remote per-frame DoS amplifier)
Summary
rl_rxeof reacts to every frame the 8139 flags bad (CRC/runt/align - remotely generatable at line rate) with a complete rl_init: chip quiesce + register reprogram + setmulti crc32 walk + mii_mediachg MDIO bit-bang with DELAY loops + callout_reset, all in intr context holding the serializer and flushing all queued TX; the SYSTEM_ERR path additionally buys a 10ms busy-wait. A remote peer sending corrupted frames at line rate (~148k/s) keeps the adapter in a permanent re-init cycle. Guest-verified unreachable on QEMU's model (always sets ROK) - honest inconclusive; live functional evidence for the surrounding path. Advance-past-descriptor fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4235 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rl_live_test.sh | β | 1.5 KB | view raw | |
| rl_repro.sh | β | 790 B | view raw | |
| run.log | β | 1.0 KB | view raw | |
| guest_env.txt | β | 1.1 KB | view raw | |
| env.txt | β | 533 B | view raw | |
| fix.diff | β | 925 B | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| README.md | β | 2.8 KB | β raw | |
| verdict.json | β | 3.4 KB | view raw |
DF-4235 β rl(4): every RX-error frame triggers a full adapter re-init under the ifnet serializer (remote per-frame DoS amplifier)
File: sys/dev/netif/rl/if_rl.c (pass 1 verification β first finding for this file)
Severity: Low (single-NIC remote availability degradation)
Status: source-certain; runtime inconclusive on the guest (QEMU's
rtl8139 model cannot inject RX_ERR frames β it accepts every frame with ROK,
so the error branch cannot be staged)
The bug
rl_rxeof() (if_rl.c:1028-1032):
if ((rxstat & RL_RXSTAT_RXOK) == 0) {
IFNET_STAT_INC(ifp, ierrors, 1);
rl_init(sc); /* full adapter reset, in interrupt context */
return;
}
Any inbound frame the 8139 flags bad (CRC error, runt, alignment, FIFO
overflow β¦ β all remotely generatable by sending corrupted frames on the
wire) costs a complete rl_init(): rl_stop() (TX quiesce + free all
pending TX mbufs), full register reprogram, rl_setmulti() (TAILQ walk +
CRC32 per multicast entry), mii_mediachg() (PHY reprogram through MDIO
bit-banging with DELAY(1) loops β hundreds of PIO reads/writes on 8129
class parts), and a fresh callout_reset β all inside the interrupt (or
ifpoll) context while holding the interface serializer. At 100 Mbit/s line
rate a remote attacker can deliver ~148k minimum-size corrupted frames/s;
each one pays the full re-init and drops every co-pending frame, so a
trivially cheap packet stream keeps the interface permanently down/cycling
(rl_intr RX_ERR β rl_rxeof β rl_init β repeat). Note RL_ISR_SYSTEM_ERR
additionally arms the up-to-10 ms rl_reset() busy-wait (if_rl.c:690-703,
RL_TIMEOUT=1000 Γ DELAY(10)), also under the serializer.
Sibling drivers moved to "count, skip descriptor, continue" for ordinary RX errors decades ago; rl still hard-resets per frame.
Fix
fix.diff: on ordinary per-frame RX errors just account + advance
(keep the re-init only for FIFO-overflow / system-error class conditions,
which the ISR path already handles via RL_ISR_SYSTEM_ERR).
Live evidence gathered (supporting, this pack)
The guest does emulate an rtl8139 (dfbsd-qemu/vm_rl.sh temp launcher),
so the driver's normal RX/TX paths were exercised live for this audit:
attach, ~1250 packets of DNS traffic through rl_rxeof/rl_encap, promisc,
media change, BPF, 600 down/up cycles β zero memory-safety events (see
rl_live_test.sh, rl_repro.sh, guest_env.txt, and the DF-4236 pack for
the churn behavior). That live run doubles as negative evidence for the
length-trust analysis: DFly's m_devget() chunks arbitrary lengths and the
uint16_t cur_rx arithmetic wraps exactly at RL_RXBUFLEN (65536), so
rl_rxeof is memory-safe even against a lying device β the only defect on
that path is this Low-severity re-init amplifier.
DF-4235 VERDICT β per-frame rl_init() on RX errors (remote DoS amplifier)
Verdict: source-certain (Low); runtime inconclusive β QEMU's rtl8139 model accepts every frame with ROK set (no CRC/runt error injection), so the error branch cannot be staged on the audit guest.
Trace
rl_intr() if_rl.c:1242
:1270 if (status & RL_ISR_RX_ERR) rl_rxeof(sc);
rl_rxeof() if_rl.c:979
:1028 if ((rxstat & RL_RXSTAT_RXOK) == 0) {
:1029 IFNET_STAT_INC(ifp, ierrors, 1);
:1030 rl_init(sc); <-- FULL re-init, in intr ctx, serializer held
:1031 return;
rl_init() (if_rl.c:1388-1491) cost per bad frame:
- rl_stop() (:1400 β :1580-1607): chip quiesce + free all pending TX
mbufs (drops co-queued frames),
- CSR_WRITE_STREAM_4 MAC reprogram, rl_list_tx_init, RX/TX config,
- rl_setmulti() (:1458 β :640-687): multicast TAILQ walk + per-entry
ether_crc32_be,
- mii_mediachg() (:1483): PHY reprogram β on 8129 parts via
rl_mii_writereg MDIO bit-banging with dozens of DELAY(1) PIO cycles
(if_rl.c:483-518),
- callout_reset (:1490).
All of it inside the interrupt handler (or ifpoll compat) while holding
ifp->if_serializer. A remote sender emitting corrupted frames (bad CRC β
trivially crafted on the wire) at line rate keeps the adapter in a
permanent re-init cycle: every in-flight TX queue is flushed per frame, the
serializer is held for the whole reprogram, and the effective throughput of
the box on that serializer domain collapses. RL_ISR_SYSTEM_ERR frames
(if_rl.c:1276-1279) add rl_reset() β a busy-wait of up to
RL_TIMEOUT Γ DELAY(10) = 10 ms (if_rl.c:696-700, if_rlreg.h:407).
Per the audit rubric this is Low: single-NIC availability, needs a peer on the wire, no memory-safety impact.
Why inconclusive at runtime
The audit guest attaches a QEMU-emulated rtl8139 (see guest_env.txt:
rl0@pci0:0:4:0 chip=0x813910ec), and rl_rxeof was exercised live over
~1250 frames β but QEMU's model computes no CRC and pads/strips rather than
erroring, so RL_RXSTAT_RXOK is always set on delivery and the :1028
branch is unreachable from the guest. No hw.rl sysctl or register poke is
available to force it either. Honest classification: inconclusive (impact
demonstrable only on real hardware or a fault-injecting model).
Adjacent negative results established during this live session
rl_rxeoflength trust is not a memory-safety bug on DFly:m_devget()(sys/kern/uipc_mbuf.c:2235) chunks arbitraryleninto cluster-sized bcopy pieces (no mbuf overrun possible),cur_rxisuint16_tandRL_RXBUFLENis1<<16(if_rlreg.h:300-301) so the missing%wraps exactly on ring size, and therx_bytes > max_byteswindow plus the wrap-branchm_copybackfromrl_rx_bufall stay inside the 64 KB ring even for a lying length field.- TX side:
ether_ioctlcapsSIOCSIFMTUatETHERMTU(sys/net/if_ethersubr.c:715-723),m_defragproduces β€ MCLBYTES clusters, and the 1-segmentbus_dmamap_load_mbuf_segmentfails cleanly otherwise β the 4-slotrl_tx_chainis always indexed mod 4 (RL_INC). - eeprom/93cx6 and MII walks are fixed-size;
rl_setmultihashes are masked; no ioctl copies unvalidated user lengths (rl_ioctldelegates toifmedia_ioctl/ether_ioctl).
Fix
fix.diff β count-and-skip instead of re-init for ordinary frame errors.
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 advances CAPR past the bad descriptor using the status word's length field (valid for error frames per datasheet) and continues the loop. Not runtime-validable: the error branch cannot be staged on QEMU's model.
fix.diff; run.log (ROK always set on guest deliveries)
Confirmed kernel references
Detail
Exploit chain
remote attacker on the wire -> line-rate bad frames -> per-frame full re-init under serializer -> interface effectively down + one core pinned; no memory-safety impact (Low). RL_ISR_SYSTEM_ERR frames additionally buy a up-to-10ms rl_reset busy-wait (if_rl.c:690-703).
Evidence (decisive lines)
run.log (functional live evidence + RX_ERR unreachability), guest_env.txt (attach proof), rl_live_test.sh/rl_repro.sh, DF-4236/run.log (full churn logs), VERDICT.md (cost breakdown path:line), fix.diff
PoC changes
No seed (first finding for this file). Live traffic harness authored (rl_live_test.sh) using slirp's virtual DNS (10.0.2.3) as an rl0-scoped peer so the ssh route via vtnet0 is never touched.
Verified recommended fix
On ordinary per-frame RX errors, account and skip the descriptor instead of rl_init(); keep reset-class handling on RL_ISR_SYSTEM_ERR (see fix.diff).
Verdict
Source-certain Low: rl_rxeof() reacts to ANY inbound frame the 8139 flags bad by running a full rl_init() in interrupt context under the ifnet serializer (if_rl.c:1028-1032) -- chip quiesce + register reprogram + rl_setmulti TAILQ/crc32 walk + mii_mediachg MDIO bit-bang with DELAY(1) loops + callout_reset, flushing all queued TX. A remote peer sending corrupted (bad-CRC/runt) frames at 100Mbit line rate (~148k/s) keeps the adapter in a permanent re-init cycle: cheap remote DoS amplifier on real hardware. Runtime honestly inconclusive: the audit guest DOES attach a QEMU-emulated rtl8139 (rl0@pci0:0:4:0, chip 0x813910ec) and ~1250 frames were pushed through rl_rxeof live, but QEMU's model delivers every frame with ROK set (no CRC/runt error injection), so the error branch is unreachable on this guest. The live session doubles as negative evidence that rl_rxeof's length handling is memory-safe (m_devget chunking, uint16_t cur_rx == RL_RXBUFLEN wrap).
No comments yet.