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)
PoC verification
Evidence pack
findings/poc/DF-4304 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_rxeofspins 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 - 4underflows β error branch), and bumps ierrors β an mbuf-alloc storm at full interrupt priority.bfe_txeofspins the same way freeing nothing; if the phantom walk crosses an empty ring it first tripsKKASSERT(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
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 carriesBFE_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 bylen > 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_testablefix.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
Confirmed kernel references
- sys/dev/netif/bfe/if_bfe.c:1064
- sys/dev/netif/bfe/if_bfe.c:1070
- sys/dev/netif/bfe/if_bfe.c:1080
- sys/dev/netif/bfe/if_bfe.c:1106
- sys/dev/netif/bfe/if_bfe.c:1109
- sys/dev/netif/bfe/if_bfe.c:549
- sys/dev/netif/bfe/if_bfe.c:1176
- sys/dev/netif/bfe/if_bfe.c:1190
- sys/dev/netif/bfe/if_bfereg.h:69
- sys/dev/netif/bfe/if_bfereg.h:405
- sys/dev/netif/bfe/if_bfereg.h:426
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.
No comments yet.