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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4235 Β· 9 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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_rxeof length trust is not a memory-safety bug on DFly: m_devget() (sys/kern/uipc_mbuf.c:2235) chunks arbitrary len into cluster-sized bcopy pieces (no mbuf overrun possible), cur_rx is uint16_t and RL_RXBUFLEN is 1<<16 (if_rlreg.h:300-301) so the missing % wraps exactly on ring size, and the rx_bytes > max_bytes window plus the wrap-branch m_copyback from rl_rx_buf all stay inside the 64 KB ring even for a lying length field.
  • TX side: ether_ioctl caps SIOCSIFMTU at ETHERMTU (sys/net/if_ethersubr.c:715-723), m_defrag produces ≀ MCLBYTES clusters, and the 1-segment bus_dmamap_load_mbuf_segment fails cleanly otherwise β€” the 4-slot rl_tx_chain is always indexed mod 4 (RL_INC).
  • eeprom/93cx6 and MII walks are fixed-size; rl_setmulti hashes are masked; no ioctl copies unvalidated user lengths (rl_ioctl delegates to ifmedia_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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4235

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).