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

bfe_rxeof/bfe_txeof completion walks trust the 12-bit DMA current-descriptor field with no range validation and no work bound: encodable off-ring index 511 (byte offsets 4088..4095) makes the interrupt loop non-terminating β€” serializer-held livelock / KKASSERT panic

Summary

Both walks decode the 12-bit CDMASK byte offset into a descriptor index used as the loop termination target. The field encodes indices 0..511 but the rings hold 511 descriptors (valid 0..510) and BFE_INC cycles software indices over 0..510 only - any hardware-reported offset in [4088..4095] makes the while-loop non-terminating in interrupt context holding the serializer: rxeof spins allocating clusters/DMA maps forever; txeof spins or trips the KKASSERT into a panic. No range check, no iteration bound. The off-ring encoding is architecturally used (the driver itself writes 511*8 into the RX pointer). Harness-enumerated index space on the guest; hardware-gated. Range-validate + cap fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4304 Β· 11 files
FileTypeDescriptionSize
README.md β€” 4.0 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
bfe_walk_harness.c β€” 2.9 KB view raw
build.sh β€” 108 B view raw
run.sh β€” 213 B view raw
build.log β€” 130 B view raw
run.log β€” 916 B view raw
env.txt β€” 1.2 KB view raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 5.1 KB view raw

DF-4304 β€” bfe_rxeof/bfe_txeof walks trust the 12-bit DMA "current descriptor" field with no range validation and no work bound: an encodable off-ring index (511) makes the interrupt loop non-terminating (serializer-held livelock)

File: sys/dev/netif/bfe/if_bfe.c (pass 2; known DF-1509 not touched) Lines: 1059-1093 (bfe_txeof: chipidx decode :1064-1065, unbounded walk :1070-1083, KKASSERT(tx_cnt>0) :1080), 1097-1150 (bfe_rxeof: current decode :1106-1107, unbounded walk :1109-1147) Registers: sys/dev/netif/bfe/if_bfereg.h:69 BFE_STAT_CDMASK 0x00000fff (12-bit byte offset), :405-406 ring counts 511, :434-437 struct bfe_desc = 8 bytes, :426 BFE_INC wraps 0..510.

The bug

Both completion walks decode the hardware's "current descriptor" byte offset from DMARX_STAT/DMATX_STAT and use it directly as the loop's termination target:

bfe_txeof:  chipidx = (CSR_READ_4(sc, BFE_DMATX_STAT) & 0xfff) / 8;  /* :1064 */
            while (i != chipidx) { ...; BFE_INC(i, 511); }           /* :1070 */
bfe_rxeof:  current = (CSR_READ_4(sc, BFE_DMARX_STAT) & 0xfff) / 8;  /* :1106 */
            while (current != cons) { ...; BFE_INC(cons, 511); }     /* :1109 */

The 12-bit field encodes byte offsets 0..4095 β†’ descriptor indices 0..511, but the ring holds 511 descriptors (valid indices 0..510; EOT at 510). Software indices cycle over 0..510 via BFE_INC and can never equal 511. Any hardware-reported offset in [4088..4095] β€” i.e. index 511 β€” therefore makes the loop non-terminating:

  • bfe_rxeof spins forever in interrupt context holding the serializer: every iteration allocates a fresh cluster (bfe_newbuf), loads a DMA map, reads a zeroed header (len=0 - 4 underflows β†’ error branch), and bumps ierrors β€” an mbuf-alloc storm at full interrupt priority.
  • bfe_txeof spins the same way freeing nothing; if the phantom walk crosses an empty ring it first trips KKASSERT(sc->bfe_tx_cnt > 0) (:1080) β†’ panic instead.

There is no range check on the decoded index and no iteration bound.

The off-ring encoding is architecturally used on this very register interface: the driver itself programs 511 * 8 = 4088 into BFE_DMARX_PTR at if_bfe.c:549 (bfe_list_rx_init writes i * sizeof(struct bfe_desc) after the loop, i == 511). Nominal DMARX_STAT values stay ≀ 4087 because the EOT descriptor at 510 wraps the engine, but any transient (post-EOT increment-before-wrap), error state, or bit-flip that reports 4088..4095 converts the next RX/TX interrupt into an unkillable livelock. The RX/TX error interrupts (BFE_ISTAT_RDU etc.) that typically accompany such states are handled by bfe_init re-init before bfe_rxeof runs (:1176-1191), so the error path does not clean the wedged state.

Secondary aspect (DF-3761 family): even for in-range values the walks have no work bound β€” no process_limit-style cap like FreeBSD's alc/msk or DFly msk(4) β€” so a sustained small-frame flood keeps the walk perpetually non-empty within a single interrupt.

Proven (index-space + loop-termination harness, guest)

bfe walk harness: CDMASK=0xfff desc_sz=8 ring=511 entries (valid 0..510)
encodable descriptor indices: 0..511  -> index 511 is OUTSIDE the ring (ring max 510)
offsets encoding the off-ring index: [4088..4095]  (driver itself writes 4088 (511*8) to BFE_DMARX_PTR at if_bfe.c:549)
current=511: walk NEVER TERMINATES (no cons in 0..510 equals it; >1e6 steps)
RESULT: off-ring index 511 => bfe_rxeof/bfe_txeof walk cannot terminate (interrupt-context livelock holding the serializer)

The guest has no Broadcom PCI device (NO_BROADCOM_PCI, hw.bfe unknown oid) and QEMU provides no BCM4401 model, so the driver itself cannot be executed β€” per family convention (DF-3761/DF-3715) recorded as inconclusive; the invariant break is certain from source + harness.

Build/run (guest)

cc -o /root/bfe_walk_harness /root/bfe_walk_harness.c   # build.log
/root/bfe_walk_harness                                  # run.log
VERDICT.md
↓ download raw

DF-4304 VERDICT β€” unbounded, range-unvalidated RX/TX completion walks in bfe

Bottom line

Source-certain invariant break; runtime honestly inconclusive (hardware-gated: the guest has no BCM4401/BCM4402 and QEMU cannot emulate one β€” NO_BROADCOM_PCI, sysctl hw.bfe unknown oid; guest src md5 == audited tree). Availability-only defect (interrupt-context livelock / KKASSERT panic); no memory corruption β€” software indices never leave 0..510, so no OOB access occurs; the failure mode is non-termination.

The math (all constants from sys/dev/netif/bfe/if_bfereg.h)

  • BFE_STAT_CDMASK = 0x00000fff (:69) β€” 12-bit "current descriptor" BYTE offset in DMARX_STAT (:91) / DMATX_STAT (:68).
  • sizeof(struct bfe_desc) = 8 (:434-437, two uint32_t).
  • Decoded index range: 0..4095/8 = 0..511.
  • BFE_RX_LIST_CNT = BFE_TX_LIST_CNT = 511 (:405-406) β€” valid indices 0..510; desc 510 carries BFE_DESC_EOT (if_bfe.c:619-620).
  • BFE_INC(x, 511) (:426) cycles software indices over 0..510 β€” it can never produce 511.

β‡’ while (current != cons) (bfe_rxeof :1109) and while (i != chipidx) (bfe_txeof :1070) cannot terminate when the register reports an offset in [4088..4095]. Both decode sites (:1064-1065, :1106-1107) perform no range validation, and neither loop has an iteration bound.

Why the off-range value is expressible

  • The field's full legal encoding (12 bits) includes index 511 β€” this is not a "garbage read" argument, it is the register's own value space.
  • The driver itself writes the byte offset 4088 (= 511*8) into the SIBLING register BFE_DMARX_PTR at if_bfe.c:549 β€” "posted through end of ring" is an architecturally used value on this DMA engine.
  • Nominal operation keeps DMARX_STAT ≀ 4087 (EOT wrap at desc 510), but nothing in the driver contract guarantees the post-EOT transient (increment-before-wrap), error-state reporting, or single-bit corruption stays ≀ 4087 β€” and the driver's own error handling (ISTAT_ERRORS β†’ bfe_init, if_bfe.c:1176-1187) runs BEFORE bfe_rxeof (:1190-1191), so it cannot rescue a walk that starts with a bogus index.

Impact if triggered (real bfe(4) hardware)

  • rxeof variant: interrupt thread spins forever holding the ifnet serializer; each iteration does m_getcl + bus_dmamap_load + sync + header read (len underflow β†’ ierrors branch, bfe_setup_rxdesc) β€” a full mbuf-allocation storm at interrupt priority; one CPU pegged; interface and all its ioctls/timers wedge; system-wide degradation.
  • txeof variant: same spin; alternatively KKASSERT(sc->bfe_tx_cnt > 0) (:1080) panics first if the phantom walk crosses an empty ring.

Harness (guest, run.log)

bfe_walk_harness.c enumerates the encodable index space and simulates the BFE_INC walk: index 511 β‡’ "walk NEVER TERMINATES"; all indices ≀ 510 terminate. Compiled and executed on the guest (CC_RC=0, HARN_RC=0) β€” see run.log.

Fix

Range-validate the decoded index before walking (bfe_txeof and bfe_rxeof) β€” if (chipidx >= BFE_TX_LIST_CNT) return; with a rate-limited printf; optionally add a work bound per interrupt. fix.diff implements the range guards (patch -p1 --dry-run clean). Cannot be runtime-validated without hardware β†’ fix_status not_testable.

Negative results (adjacent candidates killed)

  • rxeof length trust (checklist DF-3604 family): KILLED. len = rxheader->len - ETHER_CRC_LEN (:1117) is uint16 arithmetic promoted then stored to uint32_t; underflow (len<4) yields a huge value caught by len > ETHER_MAX_LEN + 32 (:1121) β†’ error branch. Passing lens are ≀ 1550; cluster is MCLBYTES=2048 with BFE_RX_OFFSET=30 headroom (:1139) β‡’ max touch 1580 < 2048. BFE_RX_FLAG_ERRORS (:390-391) covers OFIFO|CRCERR|SERR|ODD; LARGE frames are covered by the len bound. No cluster OOB is constructible.
  • tx ring invariant (checklist item b): KILLED for software. bfe_encap (:1203-1268) bounds nsegs by maxsegs = 511 - tx_cnt - 2 (:1209-1212), the start/oactive checks in bfe_start (:1298-1301) keep tx_cnt + SPARE < LIST_CNT, and the map-swap (:1257-1258) is the standard DFly first/last juggle. Software indices stay in 0..510.
  • eeprom/stat walks: bounded (bfe_read_eeprom :960-962 fixed 128 bytes; bfe_stats_update :1051-1055 fixed register ranges; bfe_clear_stats :672-675 fixed).

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 authored against the read-only sys/ tree (patch -p1 --dry-run clean); cannot be runtime-validated without BCM4401 hardware.

findings/poc/DF-4304/fix.diff
↓ fix.diffper-fix-DF-4304

Confirmed kernel references

Detail

Exploit chain

Precondition: bfe(4) hardware (BCM4401/4402) whose DMARX_STAT/DMATX_STAT current-descriptor field transiently or persistently reports byte offset 4088..4095 (post-EOT increment-before-wrap transient, DMA engine error state, or register corruption). The next RX/TX interrupt enters bfe_rxeof/bfe_txeof, the walk never terminates, one CPU pegs at interrupt priority holding the serializer, the interface and its ioctls/timers wedge permanently, and the rxeof variant additionally hammers the cluster allocator - system-wide livelock until reboot. No remote packet can set the register directly; the trigger is hardware-state-dependent.

Evidence (decisive lines)

run.log (harness output: 'index 511 is OUTSIDE the ring', 'current=511: walk NEVER TERMINATES', plus presence probes NO_BROADCOM_PCI / hw.bfe unknown oid / guest-src md5 == audited tree); build.log (cc clean); bfe_walk_harness.c (index-space enumeration + BFE_INC walk simulation); VERDICT.md (path:line trace + negative results incl. the killed DF-3604-family length-trust candidate); fix.diff (range guards in both walks, patch -p1 --dry-run clean)

PoC changes

No seed PoC. Authored bfe_walk_harness.c (userland index-space + loop-termination proof of the exact driver arithmetic) because the guest has no BCM4401 and QEMU cannot emulate one; plus guest-side presence probes.

Verified recommended fix

Range-validate the decoded index in both walks (return with a rate-limited error print if chipidx/current >= BFE_xX_LIST_CNT) and optionally cap iterations per interrupt; see fix.diff.

Verdict

Source-certain invariant break, runtime honestly inconclusive (hardware-gated). bfe_txeof (sys/dev/netif/bfe/if_bfe.c:1064-1083) and bfe_rxeof (:1106-1147) decode the DMA engines' 12-bit 'current descriptor' byte offset (BFE_STAT_CDMASK 0xfff, if_bfereg.h:69) into a descriptor index and use it directly as the walk's termination target, with NO range validation and NO work bound. The field's encoding space maps to indices 0..511 (4095/8) but the rings hold 511 descriptors (valid 0..510, EOT at 510) and BFE_INC cycles software indices over 0..510 only - so any hardware-reported offset in [4088..4095] (index 511) makes while(current != cons)/while(i != chipidx) NON-TERMINATING in interrupt context while holding the ifnet serializer: bfe_rxeof spins allocating cluster mbufs and loading DMA maps forever (len=0-4 underflows into the error branch each pass); bfe_txeof spins the same way or trips KKASSERT(tx_cnt>0) (:1080) into a panic. The off-range encoding is architecturally used on this register interface - the driver itself programs 511*8=4088 into BFE_DMARX_PTR at if_bfe.c:549 - and the driver's error-reinit path (ISTAT_ERRORS -> bfe_init, :1176-1187) runs BEFORE bfe_rxeof (:1190) so it cannot rescue a walk begun with a bogus index. Guest index-space/loop-termination harness proves index 511 never terminates (run.log, HARN_RC=0); the guest has no Broadcom PCI device and QEMU provides no BCM4401 model (env.txt: NO_BROADCOM_PCI, hw.bfe unknown oid), so the instance cannot be fired - per family convention (DF-3761/DF-3715) recorded inconclusive. Availability only: software indices never leave 0..510, so no OOB memory access - no corruption, no uid=0 route.