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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4068 Β· 12 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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 inconclusive with source-level confidence certain.

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_events array indexing is masked (consumer mod 256) β€” the event-ring access is in-bounds; only termination is unbounded.
  • ti_start's prodidx comes from a host-written PCI register (TI_MB_SENDPROD_IDX), not DMA β€” values are driver-maintained in [0,512).
  • ti_encap TX 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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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