ti_rxeof/ti_txeof/ti_handle_events terminate on raw equality with NIC-DMA'd 32-bit producer/consumer words: one out-of-range word makes the loop condition unsatisfiable β interrupt-context livelock with host interrupts masked and the ifnet serializer held (DF-3761 unbounded-walk family, ti(4) instance)
Summary
The three completion walks compare a driver-side consumer (uint16_t, confined to the ring modulus) against the raw 32-bit producer words the firmware DMA-writes into the ring block. Any DMA'd value outside the modular range can never equal the consumer: the loop spins forever inside ti_intr AFTER host interrupts were masked and while holding if_serializer - permanent CPU livelock; txeof additionally decrements ti_txcnt each spin. No work bound exists in any of the three loops; stronger than the alc DF-3761 shape because a single poisoned word suffices (no flood needed). Guest-executed termination model (benign walks terminate; poisoned words never within 8x-ring budget); hardware-gated. Ring-sized iteration-budget fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4068 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| trigger.c | β | 3.4 KB | view raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 159 B | view raw | |
| build.log | β | 464 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| env.txt | β | 977 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| fix_build.log | β | 998 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-4068 β ti completion walks keyed on raw equality with DMA'd u32 index
Finding (file under audit: sys/dev/netif/ti/if_ti.c, pass 2)
All three ti_intr() completion loops terminate only on raw equality between a driver-side consumer index (uint16_t, held inside the ring modulus by TI_INC(x,y) = (x+1)%y, if_tireg.h:965) and a 32-bit index word that the Tigon firmware DMA-writes into host memory (struct ti_producer.ti_idx, if_tireg.h:485-488 β the NIC owns ti_return_prodidx_r / ti_tx_considx_r / ti_ev_prodidx_r inside the contigmalloc'd ti_ring_data):
ti_rxeof: while (sc->ti_rx_saved_considx !=
sc->ti_return_prodidx.ti_idx); if_ti.c:1629
ti_txeof: while (sc->ti_tx_saved_considx !=
sc->ti_tx_considx.ti_idx); if_ti.c:1729
ti_handle_events: while (sc->ti_ev_saved_considx !=
sc->ti_ev_prodidx.ti_idx); if_ti.c:511
The consumer only ever takes values in [0, ring_cnt): 2048 / 512 / 256. A DMA'd index word OUTSIDE that range (e.g. 0x8000, 0xFFFFFFFF β faulty firmware or DMA tampering) makes equality unsatisfiable: the loop never exits. It runs inside the interrupt handler AFTER host interrupts were masked (if_ti.c:1778, TI_MB_HOSTINTR=1) while holding ifp->if_serializer: hard CPU livelock. txeof additionally decrements sc->ti_txcnt (int, if_ti.c:1755) each spin.
No DF-3761-style work bound exists anywhere in the three loops.
Build / run (audit guest)
build.sh # cc -O2 -Wall -Wextra -o /tmp/df4068 /tmp/df4068.c run.sh # /tmp/df4068
Expected: benign cases terminate; poisoned 32-bit index words never terminate within 8x the ring budget (RC_DF4068_MODEL=0). The model does NOT touch the kernel; the real path needs Tigon hardware writing an out-of-range index word.
Hardware gate (why status=inconclusive)
env.txt: audit guest has no Tigon-family PCI device; if_ti.ko cannot attach. Family convention: inconclusive.
Fix (fix.diff, compile-validated)
Bound each walk with a ring-sized iteration budget (the standard work-bound fix for this class β cf. the alc DF-3761 family): after TI_RETURN_RING_CNT / TI_TX_RING_CNT / TI_EVENT_RING_CNT iterations the handler returns, interrupts are re-enabled, and the next interrupt resumes. git apply --check clean; patched module builds on the guest with -Werror (fix_build.log: FIXBUILD_f3_RC=0). Runtime fix validation not possible without hardware (fix_status=not_testable).
DF-4068 β VERDICT
Status: inconclusive (hardware-gated) Β· source-level: certain Β· severity: Medium Β· bucket: dos (livelock)
Root cause
The Tigon firmware publishes completion progress by DMA-writing 32-bit
index words into host memory (struct ti_producer.ti_idx,
sys/dev/netif/ti/if_tireg.h:485-488; pointers handed to the NIC at
if_ti.c:1248-1249, 1323-1324, 1357-1358). All three interrupt-path
walks use these words as raw loop terminators:
while (sc->ti_rx_saved_considx != sc->ti_return_prodidx.ti_idx) /* if_ti.c:1629 */
while (sc->ti_tx_saved_considx != sc->ti_tx_considx.ti_idx) /* if_ti.c:1729 */
while (sc->ti_ev_saved_considx != sc->ti_ev_prodidx.ti_idx) /* if_ti.c:511 */
The driver-side indices are uint16_t (if_tireg.h:1141-1143) and
TI_INC(x, y) = (x + 1) % y (if_tireg.h:965) keeps them inside
[0, 2048) / [0, 512) / [0, 256). A DMA'd word whose value lies
outside that modular range β high bit set, or simply >= ring size as a
raw integer (e.g. 4096 = 2Γ2048, 0x8000, 0xFFFFFFFF) β can never equal
the consumer, so the loop spins forever.
Where it spins: inside ti_intr() after host interrupts were masked
(CSR_WRITE_4(sc, TI_MB_HOSTINTR, 1), if_ti.c:1778) and while holding
ifp->if_serializer (intr set up with the ifnet serializer,
if_ti.c:1567-1569). Effect: the servicing CPU never returns from the
IRQ β hard livelock / soft lockup; the serializer is never released, so
anything else touching the interface stalls too. ti_txeof compounds
it by decrementing sc->ti_txcnt (int, if_ti.c:1755) once per spin.
This is the DF-3761 (alc unbounded-walk) class: a completion walk with no work bound, keyed on device-fed state. Note the alc instance was "perpetually non-empty ring"; here the stronger form holds β the termination condition itself is unsatisfiable, so even ONE poisoned word (no flood needed) wedges the loop permanently.
Why the "firmware always writes mod ring size" assumption fails
Nothing in the driver or the DMA contract enforces the range: the word is 32 bits, the modulus is 2048/512/256. A firmware bug (index corruption, ring-size misconfiguration after a partial reset), a malicious peripheral, or PCIe/Thunderbolt DMA tampering writes an out-of-range value once and the CPU is wedged. Same trust boundary as DF-4066/DF-4067 β the kernel must bound its own work regardless of what the device claims.
Verification narrative
trigger.c(guest, RC=0): simulates both the benign walks (producer inside the modular range β terminate in 37/511/3 iterations) and the poisoned ones (prod_raw = 0x8000 / 4096 / 0x10000 / 0xFFFFFFFF β NEVER terminates within a budget of 8Γ ring size; run.log).- env.txt: no Tigon-family PCI device on the audit guest, so the real
DMA'd word cannot be instantiated; per family convention the runtime
verdict is
inconclusivewith source-level confidencecertain.
Fix
fix.diff: wrap each of the three walks with a ring-sized iteration
budget (for (int budget = TI_..._RING_CNT; cond && budget > 0;
budget--)). This is the standard fix shape for this class (it is
what FreeBSD bounds alc(4)/msk(4) walks with): a poisoned word then
costs at most one ring's worth of work per interrupt instead of the
CPU. git apply --check clean; patched module builds on the guest
with -Werror (fix_build.log FIXBUILD_f3_RC=0); runtime validation
needs hardware (fix_status=not_testable).
Cleared in this pass (adjacent walk/bound surfaces)
ti_handle_eventsarray indexing is masked (consumer mod 256) β the event-ring access is in-bounds; only termination is unbounded.ti_start'sprodidxcomes from a host-written PCI register (TI_MB_SENDPROD_IDX), not DMA β values are driver-maintained in [0,512).ti_encapTX walk is bounded by the 16-descriptor safety margin and masked indices (if_ti.c:1844-1895).- Tigon-1 banked TX window mapping (WINBASE remap +
idx % 128) is in-bounds in both txeof and encap (if_ti.c:1733-1748, 1846-1859).
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 adds ring-sized work budgets to all three walks (standard bound for the DF-3761 class); git apply --check passes and the patched if_ti.ko builds clean on the guest with -Werror -std=gnu11 (fix_build.log FIXBUILD_f3_RC=0); runtime validation impossible without Tigon hardware.
findings/poc/DF-4068/fix.diff + findings/poc/DF-4068/fix_build.log
Confirmed kernel references
- sys/dev/netif/ti/if_ti.c:511
- sys/dev/netif/ti/if_ti.c:1629
- sys/dev/netif/ti/if_ti.c:1729
- sys/dev/netif/ti/if_ti.c:1755
- sys/dev/netif/ti/if_ti.c:1778
- sys/dev/netif/ti/if_ti.c:1567
- sys/dev/netif/ti/if_ti.c:1248
- sys/dev/netif/ti/if_ti.c:1323
- sys/dev/netif/ti/if_ti.c:1357
- sys/dev/netif/ti/if_tireg.h:485
- sys/dev/netif/ti/if_tireg.h:965
- sys/dev/netif/ti/if_tireg.h:1141
Detail
Exploit chain
On real (faulty/malicious) Tigon/Tigon2 hardware or PCIe DMA tampering: firmware/DMA writes one out-of-range 32-bit value into ti_return_prodidx_r (or ti_tx_considx_r / ti_ev_prodidx_r) -> next interrupt enters ti_rxeof/ti_txeof/ti_handle_events, masks host interrupts, spins forever holding the serializer -> permanent system livelock (all cores touching the serializer or the IRQ line starve; watchdog cannot preempt a spinning hard IRQ context).
Evidence (decisive lines)
run.log (benign walks terminate; poisoned 0x8000/0x1000/0x10000/0xffffffff walks NEVER TERMINATE in budget, RC=0); build.log; env.txt (no Tigon PCI devices on guest); VERDICT.md (path:line trace + cleared walk surfaces); trigger.c; fix.diff; fix_build.log (patched module compiles -Werror clean on guest)
PoC changes
No seed PoC (no prior findings on this file). Authored trigger.c (bounded-budget termination simulator) per the DF-4024 convention; honest hardware-absent verification.
Verified recommended fix
Give each completion walk a ring-sized iteration budget (for budget = TI_..._RING_CNT; cond && budget > 0; budget--) so a poisoned DMA'd index costs one ring's work per interrupt instead of the CPU, see fix.diff.
Verdict
Source-level certain, runtime hardware-gated (inconclusive per family convention). All three ti_intr() completion walks terminate on raw equality between a driver-side consumer index (uint16_t, kept in [0,ring_cnt) by TI_INC modulo, if_tireg.h:965) and a 32-bit word the Tigon firmware DMA-writes into host memory (struct ti_producer.ti_idx, if_tireg.h:485-488): ti_rxeof if_ti.c:1629 (ring 2048), ti_txeof if_ti.c:1729 (ring 512), ti_handle_events if_ti.c:511 (ring 256). A DMA'd value outside the consumer's modular range (high bit set, or >= ring size as a raw integer: 4096, 0x8000, 0xFFFFFFFF) makes equality unsatisfiable, so the loop never exits. It spins inside the interrupt handler AFTER host interrupts were masked (TI_MB_HOSTINTR=1, if_ti.c:1778) while holding ifp->if_serializer (intr registered with the ifnet serializer, if_ti.c:1567-1569): the CPU never returns from the IRQ -- hard livelock; txeof additionally decrements sc->ti_txcnt (int, if_ti.c:1755) each spin. No work bound exists anywhere in the three loops (DF-3761 alc unbounded-walk class, stronger form: the termination condition itself is unsatisfiable, one poisoned word suffices, no flood needed). Runtime: audit guest has zero Tigon-family PCI devices (env.txt), so the poisoned word cannot be instantiated; trigger.c simulates benign (terminates in 37/511/3 iterations) and poisoned (never terminates in 8x ring budget) walks on the guest (run.log RC=0).
No comments yet.