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

Inverted RX descriptor-ownership test in wb_rxeof(): receive path completely broken, per-packet chip reset/reinit storm from interrupt context

Summary

wb_rxeof walks the RX ring while OWN is SET and breaks when CLEAR - the exact inversion of the W89C840F ownership protocol (verified against the FreeBSD 4.11 original and Linux winbond-840). On real hardware: completed frames hit the break and are never delivered (ipackets stays 0); the driver validates descriptors the DMA engine is concurrently writing (torn status/length reads) hitting the 'receiver babbling' branch which re-arms a chip-owned descriptor and runs chip reset+init from the ISR; independently the chip starves and RX_NOBUF triggers full 64-mbuf RX re-init from interrupt context for every packet on the wire. Unauthenticated layer-2-adjacent DoS from a single broadcast ARP. Deterministic logic harness on the guest (DFLY: processed=0 | FBSD411-COND: processed=1). Flip-the-condition fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4266 Β· 23 files
FileTypeDescriptionSize
df4266_wb_poc.c β€” 22.5 KB view raw
Makefile β€” 113 B ↓ download
kld_build.sh β€” 562 B view raw
kld_run.sh β€” 1.5 KB view raw
kld_build.log β€” 913 B view raw
build.log β€” 913 B view raw
kld_run_baseline.log β€” 4.5 KB view raw
run.log β€” 4.5 KB view raw
kld_run_fixed.log β€” 3.4 KB view raw
fix_run.log β€” 3.4 KB view raw
fix_build_tail.log β€” 1.8 KB view raw
fix_build.log β€” 1.8 KB view raw
panic_rig_v1.txt β€” 1.5 KB view raw
env.txt β€” 1.4 KB view raw
VERDICT.md β€” 9.0 KB ↓ raw
README.md β€” 3.0 KB ↓ raw
wb_logic.c β€” 4.2 KB view raw
run.sh β€” 240 B view raw
logic_run.log β€” 719 B view raw
fix.diff β€” 371 B view raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 8.5 KB view raw
code_hash.txt β€” 65 B view raw

DF-4266 β€” inverted RX descriptor-ownership test in wb_rxeof() (if_wb.c:972-975)

Verdict: REPRODUCED (kernel-level, production-handler dispatch) β€” fixed by fix.diff (kernel A/B). Impact: unauthenticated adjacent-network RX DoS (100% RX loss + per-interrupt chip reset/re-init storm) on Winbond W89C840F / Compex RL100-ATX NICs; ownership-protocol break that races the DMA engine (memory-safety tail tracked as DF-4268).

Primary evidence (this revision): production-handler KLD dispatch

The audit guest has no W89C840F NIC, but device wb is built into its X86_64_GENERIC kernel, so the rig dispatches the running kernel's own static wb_rxeof/wb_init/wb_stop (nm-resolved, sysctl-injected) against a fabricated post-attach softc, with the "chip" simulated by descriptor write-backs (the driver's only observation point of the NIC).

  • Build (in guest): sh /root/df4266/kld_build.sh β†’ make -m /usr/src/share/mk
  • Run (baseline): sh /root/df4266/kld_run.sh baseline
  • Run (fixed kernel): apply fix.diff to /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel, reboot, re-run sh /root/df4266/kld_run.sh fixed
  • Expected baseline (stock kernel): S1 BUG CONFIRMED (chip-completed frame dropped: delivered=0, ipackets=0, descriptor untouched) Β· S1fix (identical state delivered by the one-line-fixed comparator) Β· S2 BUG CONFIRMED (chip-owned descriptor processed per dispatch: ierrors++, "receiver babbling", full 64-mbuf ring teardown/re-init, ~65 ms vs 9-13 Β΅s fixed) Β· S3 BUG CONFIRMED (frame accepted from OWN=1 descriptor + re-armed under the chip) Β· SURVIVED.
  • Expected fixed kernel: S1 DELIVERED by the real patched handler (ipackets=1); S2 dispatches idle-break in Β΅s with zero side effects; S3 descriptor untouched; all bug-expectation markers "UNEXPECTED" (= fixed).

Historical evidence (prior pack revision, kept): logic harness

wb_logic.c β€” deterministic userspace transcription of both loop conditions, compiled and run on the DragonFly guest itself: DFLY loop processes 0 completed frames; FreeBSD-4.11-condition processes exactly the completed one (logic_run.log).

Files

  • df4266_wb_poc.c / Makefile β€” the KLD dispatch rig (primary PoC)
  • kld_build.sh, kld_run.sh β€” exact build/run commands
  • kld_build.log β€” full clean -Werror module build
  • kld_run_baseline.log β€” full dmesg of the baseline run (stock kernel #0)
  • kld_run_fixed.log β€” full dmesg of the fixed-kernel run (#1, same rig)
  • fix_build_tail.log β€” patched-kernel rebuild evidence (if_wb.c compile + install)
  • panic_rig_v1.txt β€” rig v1 NULL-subqueue panic (rig bug, not the finding)
  • env.txt β€” kernels, compiler, symbols, source identity (MD5 vs audit tree)
  • VERDICT.md β€” full narrative incl. method, layout cross-check, A/B table
  • wb_logic.c, run.sh, logic_run.log β€” prior logic harness (kept)
  • fix.diff β€” the one-line fix (restores FreeBSD 4.11 loop direction)
  • manifest.json, verdict.json, code_hash.txt (sha256 of df4266_wb_poc.c)
VERDICT.md
↓ download raw

VERDICT β€” DF-4266 (inverted RX OWN test in wb_rxeof)

Status: reproduced β€” at the strongest fidelity possible without the NIC: the production handler itself (the running kernel's static wb_rxeof(), resolved from /boot/kernel/kernel via nm(1) and dispatched from a KLD rig) exhibited every claimed defective behavior on the guest, and a kernel rebuilt with fix.diff exhibited none of them. Same rig binary (.ko) on both kernels; the only variable was the one-line fix.

Method β€” production-handler KLD dispatch (DF-4106 vge evidence class)

The guest has no W89C840F/Compex NIC, but device wb is built into X86_64_GENERIC (sys/config/X86_64_GENERIC:246), so the driver's real static functions live in the running kernel image. The rig (findings/poc/DF-4266/df4266_wb_poc.c, built clean with -Werror, kld_build.log):

  • resolves wb_rxeof / wb_init / wb_stop from /boot/kernel/kernel (sysctl-injected hex addresses β€” wb_newbuf exists only as an IPA-SRA clone wb_newbuf.isra.5 with a compiler-mangled ABI and is deliberately never called; the ring is armed by dispatching the kernel's own wb_init(), whose inlined wb_list_rx_init/wb_newbuf build the exact production post-init state);
  • fabricates a post-attach softc in module .bss: real wb_list_data (contiguous rxbufs + descriptor rings, armed by the REAL wb_init β€” 64 MGETHDR mbufs, descriptors WB_RXSTAT=0x80000300 FIRSTFRAG|LASTFRAG|OWN), a fake miibus device_t whose softc is a zeroed mii_data (empty phys list: mii_mediachg/mii_pollstat/mii_tick degenerate to no-ops so wb_init/wb_reset/wb_fixmedia run their full real bodies safely), a real lwkt serializer held across dispatches exactly as wb_intr would, per-CPU ifdata, a real ifaltq_subque (rig v1 panicked in wb_stop+0x156 on a NULL if_snd.altq_subq β€” rig bug, fixed by replicating if_attach's subqueue setup, net/if.c:592-640; see panic_rig_v1.txt), and a scratch page as the MMIO BAR;
  • simulates the NIC the only way the driver can observe it: descriptor status write-backs + frame bytes into the armed buffer β€” bit-exact with the W89C840F protocol (wb_newbuf arms OWN=1; chip clears OWN and sets RXCMP|FIRSTFRAG|LASTFRAG|len on completion, if_wbreg.h:227-243);
  • struct-layout agreement between the module compile and the running kernel is verified: sizeof(wb_softc)=0x1e80 if_timer=0x6a wb_bhandle=0x3c0 wb_btag=0x3c8 wb_ldata=0x400 rx_chain[0]=0x448 wb_stat_timer=0x1e68 if_snd.altq_subq=0x210 β€” every constant matches the kernel's own disassembly of wb_stop/wb_init (kld_run_baseline.log line 2).
  • The one transcription (df_wb_rxeof_fixed) is NOT the code under test β€” it is the verbatim counterfactual with exactly the fix.diff line applied, used for the module-level A/B (S1fix) and as an independent sanity check.

Baseline run β€” stock kernel #0 (Jul 2 2026), REAL wb_rxeof (kld_run_baseline.log)

  • S1 β€” chip-completed frame DROPPED (THE bug). Slot 0 write-back 0x40400300 (OWN=0, RXCMP|FIRSTFRAG|LASTFRAG, len 64) + 60-byte frame with marker payload in the armed buffer. Real wb_rxeof broke immediately at if_wb.c:974-975: delivered=0 ipackets=0 ierrors=0, descriptor NOT re-armed (0x40400300 unchanged), head NOT advanced. On silicon this is permanent: 100% RX loss, ipackets stays 0 forever, and since completed descriptors are never recycled the chip starves β†’ WB_ISR_RX_NOBUF β†’ wb_intr (if_wb.c:1137-1144) runs wb_reset+wb_init (full 64-mbuf ring teardown/re-init) for every arriving frame. Any adjacent L2 frame (untargeted broadcast suffices) triggers it: unauthenticated adjacent-network DoS.
  • S1fix β€” identical state, one-line-fixed comparator: delivered=1, if_input: m_len=60 dst=ff:ff:ff:ff:ff:ff src=00:df:42:66:00:01 type=df42 pay=0e 0f 10 11 (exact crafted bytes), ipackets=1, slot re-armed to 0x80000300. The state was processable; only the inverted test blocks the production handler.
  • S2 β€” chip-OWNED descriptors PROCESSED + per-interrupt reset storm. Armed idle ring (exactly what wb_init leaves). Each of 3 dispatches of the REAL handler: popped the chip-owned head, ierrors++, kernel printed receiver babbling: possible chip bug, forcing reset (if_wb.c:989-990), ran wb_fixmedia+wb_reset+wb_init β€” full ring re-init observable via the replaced wb_rx_chain[0].wb_mbuf pointer each dispatch β€” at ~60.9/69.8/65.3 ms per dispatch (mostly the MMIO-stand-in reset poll; the 64-mbuf teardown/reinit work is the same on real HW). On the fixed kernel the same dispatches took 9-13 Β΅s and did nothing (below).
  • S3 β€” frame ACCEPTED from a descriptor the chip STILL OWNS. Slot 0 = 0xc0400300 (OWN=1 β€” the mid-write-back transient the inverted loop wrongly accepts, i.e. what the driver sees when the interrupt fires while DMA is in flight). Real wb_rxeof processed it as a good frame (delivered=1, ipackets=1 β€” with the correct payload), then re-armed the descriptor under the chip (status rewritten to 0x80000300, wb_data re-pointed): driver and hardware now disagree about who owns descriptor 0 and its buffer. This is the on-ramp to the DMA/race memory-safety tail tracked separately as DF-4268 (not re-verified here). After the S3 delivery the loop re-entered the just-re-armed slot (OWN=1 β†’ continue, per the inversion) and babbled into yet another full reset β€” the storm feeds itself.
  • Guest stayed up (SURVIVED) β€” this finding is availability-breaking, not corruption-demonstrating (corruption family is DF-4268).

Fix validation β€” kernel #1 (Oct 14 2026), same tree + fix.diff (kld_run_fixed.log)

fix.diff applied to the guest's /usr/src (patch output in VERDICT history; fix_build_tail.log: make -j6 nativekernel + installkernel, if_wb.c recompiled at build log line 18787), rebooted into kernel #1, and the identical rig re-run β€” wb_rxeof resolved again from the new kernel (same address 0xffffffff808dae30; the one-line change did not move the symbol):

scenario baseline (buggy) fixed kernel
S1 completed frame (OWN=0) dropped, ipackets=0, not re-armed DELIVERED by the REAL patched handler (m_len=60, exact payload), ipackets=1, re-armed
S2 armed ring Γ—3 ierrors +1/dispatch, babble print, full ring re-init, 61-70 ms ierrors=0, no babble, no re-init (mbuf unchanged), 9-13 Β΅s
S3 OWN=1 looking complete accepted + re-armed under chip untouched: delivered=0, ipackets=0, status still the crafted 0xc0400300

(The fixed-run "S2 BUG CONFIRMED" banner and "S1/S1fix/S3: UNEXPECTED outcome" strings in the log are the rig's bug-expectation assertions failing β€” i.e. exactly the fixed behavior; the data lines are decisive. The run also crossed a warm reboot β€” DragonFly's msgbuf preserved the baseline section in dmesg, so both runs appear in order; attribution is by boot context and mbuf addresses, see env.txt.)

fix_status: fixed (baseline_reproduced=1, patched_reproduced=0 β€” every defective marker gone; the previously-impossible behavior β€” receiving a frame β€” now works).

Impact

Unauthenticated adjacent-network DoS on W89C840F/Compex RL100-ATX equipped hosts: 100% RX loss from the first frame + a per-interrupt full-chip reset/re-init storm (which also destroys TX progress each time), plus the S3 ownership-protocol break that races the DMA engine (DF-4268 tracks the memory-safety tail). Trigger is any Ethernet frame β€” untargeted broadcast/multicast suffices; no privileges needed. High severity stands.

Why this is "reproduced" and not "hardware-gated untested"

Everything the driver decides is a function of descriptor words and MMIO. The rig supplied both bit-exactly per the W89C840F protocol (triangulated: wb_newbuf arming semantics if_wbreg.h:243, FreeBSD 4.11 original loop, Linux winbond-840.c DescOwn convention β€” see prior pack revision), and the code that was executed was the kernel's own wb_rxeof, not a transcription. The remaining hardware dependency (the chip actually clearing OWN on write-back) is a property of the NIC the driver cannot influence β€” and S1/S3 demonstrate the driver's response to both sides of exactly that protocol event. The defective behavior is therefore demonstrated on production code, on the audit guest, deterministically, twice (plus once on the patched kernel to prove the delta).

Rig iteration notes (attempts)

  1. v1: NULL if_snd.altq_subq β†’ panic in wb_stop+0x156 (rig bug, panic_rig_v1.txt) β€” also yielded the struct-offset cross-check.
  2. v2: full baseline reproduced (all scenarios, guest survived).
  3. v3: fixed-kernel A/B β€” all defective behaviors gone; frame delivery restored by the REAL patched handler.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest's /usr/src (byte-identical to the audit tree), kernel rebuilt via make -j6 nativekernel + installkernel, rebooted into kernel #1, and the IDENTICAL rig re-run against the real patched wb_rxeof: the chip-completed frame is now DELIVERED (if_input m_len=60 with the exact crafted payload, ipackets=1, descriptor re-armed) - previously impossible; armed-ring dispatches break immediately in 9-13us with ierrors=0 and no ring re-init (vs ~65ms babble+full reset per dispatch on baseline); the OWN=1 mid-write-back descriptor is left untouched (status unchanged, delivered=0). Every defective behavior gone; no regressions observed in the run.

fix.diff; fix_build.log (if_wb.c recompiled, kernel install completed, INSTALL_DONE); kld_run_fixed.log / fix_run.log (S1 delivered by the REAL patched handler; S2 dispatches: ipackets=0 ierrors=0 ring re-inited:no elapsed 9-13us; S3: slot0.status unchanged 0xc0400300, delivered=0); fix_kernel_uname: DragonFly 6.5-DEVELOPMENT #1: Wed Oct 14 08:13:16 UTC 2026
↓ fix.diffper-fix-DF-4266

Confirmed kernel references

Detail

Exploit chain

Any adjacent Ethernet frame (untargeted broadcast/multicast suffices, no authentication/privilege) -> chip completes descriptor (OWN cleared) -> wb_rxeof breaks at once -> frame dropped + descriptor never recycled -> ring exhaustion -> WB_ISR_RX_NOBUF/RX_OK interrupts -> wb_reset+wb_init full 64-mbuf ring teardown/re-init per interrupt (permanent reset storm, TX progress destroyed each cycle, 100% RX loss). If the interrupt races DMA write-back (OWN still set, RXCMP+len visible) the inverted loop accepts the in-flight descriptor, delivers a half-DMA'd frame and re-arms the descriptor under the chip - driver/hardware ownership confusion (memory-safety tail: DF-4268). No uid0 route: availability-only on gated hardware.

Evidence (decisive lines)

kld_run_baseline.log (S1/S2/S3 BUG CONFIRMED markers + per-dispatch data: delivered=0 ipackets=0 descriptor untouched on completed frame; ierrors++/'receiver babbling'/mbuf0 replaced/full ring re-init per armed-ring dispatch at ~65ms; frame accepted from OWN=1 crafted 0xc0400300 and re-armed to 0x80000300); kld_run_fixed.log (same rig on the patched kernel: S1 delivered m_len=60 exact payload, S2 9-13us no-op, S3 status unchanged 0xc0400300); S1fix comparator A/B; env.txt (symbol addresses both kernels, MD5 source identity); VERDICT.md (method, layout cross-check vs kernel disassembly, A/B table); panic_rig_v1.txt (rig v1 NULL-altq_subqueue panic - rig bug, fixed); fix_build.log + fix.diff

PoC changes

Prior pack revision had only a userspace logic harness (wb_logic.c) and verdict untested. This run added the production-handler KLD dispatch rig (df4266_wb_poc.c): nm-resolves the kernel's own wb_rxeof/wb_init/wb_stop, fabricates the post-attach softc (static .bss wb_softc+wb_list_data, fake miibus device_t with zeroed mii_data, real lwkt serializer, per-cpu ifdata, real ifaltq_subque per if_attach net/if.c:592-640, MMIO stand-in page), arms the ring by dispatching the real wb_init, and crafts the three W89C840F descriptor states (completed write-back / armed / mid-write-back transient). Rig v1 panicked on a NULL if_snd.altq_subq inside the kernel's wb_stop - fixed by replicating if_attach's subqueue setup; recorded in panic_rig_v1.txt. Authored clean git-apply-able fix.diff (prior one had prose before the header) and validated it with a full kernel rebuild A/B.

Verified recommended fix

Invert the OWN test in wb_rxeof: break while (rxstat & WB_RXSTAT_OWN), i.e. process only chip-released descriptors - restores the FreeBSD 4.11 loop direction (fix.diff, kernel-A/B validated).

Verdict

REPRODUCED at kernel level by dispatching the REAL production handler: the audit guest's X86_64_GENERIC kernel has 'device wb' built in, so the rig resolved the running kernel's own static wb_rxeof()/wb_init()/wb_stop() from /boot/kernel/kernel (nm + sysctl injection; wb_newbuf exists only as an IPA-SRA clone and is never called directly - the ring is armed by the kernel's own wb_init) and dispatched them against a fabricated post-attach softc with real rings, a fake miibus (zeroed mii_data: all mii calls degenerate safely), a real serializer, and an MMIO stand-in page; the NIC is simulated at the driver's only observation point - descriptor write-backs - bit-exact with the W89C840F protocol (wb_newbuf arms OWN=1; chip clears OWN + sets RXCMP|FIRSTFRAG|LASTFRAG|len on completion). Baseline (stock kernel #0): S1 - a chip-COMPLETED descriptor (OWN=0, len 64, marker frame in the armed buffer) made the real wb_rxeof break IMMEDIATELY at if_wb.c:974-975: delivered=0 ipackets=0 ierrors=0, descriptor never re-armed, head never advanced - permanent 100% RX loss, and since completed descriptors are never recycled the chip starves -> WB_ISR_RX_NOBUF -> wb_intr (if_wb.c:1137-1144) runs wb_reset+wb_init (full 64-mbuf ring teardown/re-init) for every arriving frame; untargeted L2 broadcast suffices: unauthenticated adjacent-network DoS. S1fix - the IDENTICAL state handed to a verbatim one-line-fixed comparator delivered the frame (if_input m_len=60, exact crafted payload, ipackets=1, slot re-armed) - only the inverted test blocks production. S2 - an armed idle ring (all OWN=1, exactly what wb_init leaves): every dispatch PROCESSED a chip-owned descriptor (head popped, ierrors++, 'receiver babbling: possible chip bug, forcing reset' from the kernel, wb_fixmedia+wb_reset+wb_init full ring re-init per dispatch, ~65ms vs 9-13us on the fixed kernel). S3 - a mid-write-back-looking descriptor (OWN=1 + RXCMP|FIRSTFRAG|LASTFRAG|len): the real handler ACCEPTED the frame from a descriptor the chip still owns and RE-ARMED it under the chip (status rewritten to WB_RXSTAT) - ownership protocol broken end-to-end, the on-ramp to the DMA-race memory-safety tail tracked as DF-4268 (not re-verified here). Struct-layout agreement module-vs-kernel verified field-by-field against the kernel's own disassembly (sizeof 0x1e80; if_timer 0x6a; wb_bhandle 0x3c0; wb_btag 0x3c8; wb_ldata 0x400; rx_chain[0] 0x448; wb_stat_timer 0x1e68; if_snd.altq_subq 0x210). FIX VALIDATED: fix.diff applied to the guest's byte-identical /usr/src (MD5 bec15392abf0facf8fe77b4aefb23938), kernel rebuilt (make nativekernel) and rebooted, the IDENTICAL rig re-run against the real patched wb_rxeof: the completed frame is DELIVERED (ipackets=1, exact payload), armed-ring dispatches idle-break in 9-13us with zero side effects, the OWN=1 descriptor is untouched - every defective behavior gone, previously-impossible RX now works. Guest survived all runs.