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

ti_rxeof indexes the mbuf chain arrays with the NIC-DMA'd uint16_t return-descriptor ti_idx and no bounds check: OOB pointer read used as struct mbuf (wild-pointer stores at :1690 + if_input type confusion) and OOB 8-byte NULL write up to +512KB past the chain arrays inside struct ti_softc

Summary

ti_rxeof copies the 16-bit ti_idx out of the Tigon return-ring descriptor (NIC firmware DMA) and uses it raw to index ti_rx_std_chain[512]/jumbo_chain[256]/mini_chain[1024]. Any index >= the array size yields an OOB kernel WRITE (chain[rxidx] = NULL - 8 zero bytes at base+8*rxidx, up to +511,760 bytes, landing in adjacent chains, ti_jslots (corrupting ti_sc/ti_buf used by ti_jfree/ti_jref), or past the softc) plus an OOB kernel READ used as struct mbuf *m: :1690 then stores two NIC-chosen values through the wild pointer and :1706 feeds m to if_input (wild-mbuf type confusion). The stronger index variant of the DF-3604 family boundary. Guest-executed geometry model (per-path landing table); hardware-gated. Bounds-check-and-drop fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4066 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 6.8 KB ↓ raw
trigger.c β€” 6.0 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 162 B view raw
build.log β€” 464 B view raw
run.log β€” 3.9 KB view raw
env.txt β€” 977 B view raw
fix.diff β€” 1.1 KB view raw
fix_build.log β€” 998 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 5.7 KB view raw

DF-4066 β€” ti_rxeof OOB via unvalidated NIC-DMA'd return-descriptor index

Finding (file under audit: sys/dev/netif/ti/if_ti.c, pass 2)

ti_rxeof() copies the 16-bit ti_idx field out of a Tigon return-ring descriptor β€” memory the NIC firmware wrote over PCI DMA β€” and uses it directly as an index into the mbuf chain arrays, with no bounds check:

Array sizes (sys/dev/netif/ti/if_tireg.h:397-399, 1100-1102): std=512, jumbo=256, mini=1024. ti_idx is uint16_t (if_tireg.h:707/711), so a descriptor written by faulty/malicious firmware (or DMA tampering) can carry any value 0..65535.

Consequences for rxidx >= array size: 1. OOB kernel write: chain[rxidx] = NULL stores 8 zero bytes at base + 8*rxidx β€” up to +511KB past the array, landing in adjacent chain arrays, ti_jslots[] (corrupting ti_sc/ti_buf used by ti_jfree/ti_jref), or past struct ti_softc entirely (see run.log geometry table). 2. OOB kernel read used as a pointer: m = chain[rxidx] picks up whatever 8 bytes live there; if_ti.c:1690 then does m->m_pkthdr.len = m->m_len = cur_rx->ti_len β€” two stores through the wild pointer β€” and if_ti.c:1706 feeds it to ifp->if_input() (wild-mbuf type confusion through ether_input/BPF).

Attacker model: identical to the DF-3604 (bce) / DF-3989 (ale) / DF-4024 (re) / DF-4045 (stge) / DF-4055 (et) family β€” a faulty or malicious peripheral (or PCIe/Thunderbolt DMA attack surface) controls descriptor contents; the kernel must not trust them. The Tigon firmware normally echoes the driver-written r->ti_idx (if_ti.c:714/750/812), so legitimate traffic stays in bounds β€” which is exactly why this has never fired in the field but remains unguarded.

Build / run (audit guest)

build.sh   # cc -O2 -Wall -Wextra -o /tmp/df4066 /tmp/df4066.c
run.sh     # /tmp/df4066

Expected: the geometry model prints, per receive path and sample DMA'd index, where the 8-byte NULL write and the OOB pointer read land (RC_DF4066_MODEL=0). The model does NOT touch kernel memory β€” exercising the real path requires Tigon hardware.

Hardware gate (why status=inconclusive)

env.txt: the audit guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC) has zero Tigon-family PCI devices (vendors 0x12ae/0x10b7/0x1385/ 0x10a9/0x1011 absent; virtio-net only). if_ti.ko exists in /boot/kernel but cannot attach. Runtime reproduction is therefore impossible on this guest; classified per the family convention (DF-3604/DF-3989/DF-4024/DF-4045 = inconclusive).

Fix (fix.diff, compile-validated)

Bounds-check rxidx against the ring count on each of the three receive paths; on violation count an ierror and drop the descriptor (the consumer index has already advanced, so the walk stays live). git apply --check passes against the read-only tree, and the patched driver module builds clean on the guest with -Werror -std=gnu11 (fix_build.log: FIXBUILD_f1_RC=0). Runtime fix validation is not possible without the hardware (fix_status=not_testable).

VERDICT.md
↓ download raw

DF-4066 β€” VERDICT

Status: inconclusive (hardware-gated) Β· source-level: certain Β· severity: High Β· bucket: memcorrupt

What the code does

ti_rxeof() (sys/dev/netif/ti/if_ti.c:1623-1717) drains the Tigon "receive return ring". Every descriptor in that ring is written by the NIC firmware over PCI DMA β€” it is device-controlled memory from the kernel's point of view. At if_ti.c:1636-1638 the driver takes the descriptor's ti_idx field (uint16_t, sys/dev/netif/ti/if_ti.c β†’ if_tireg.h:707/711):

cur_rx = &sc->ti_rdata->ti_rx_return_ring[sc->ti_rx_saved_considx];
rxidx = cur_rx->ti_idx;
TI_INC(sc->ti_rx_saved_considx, TI_RETURN_RING_CNT);

sc->ti_rx_saved_considx (the return-ring walk index) is masked into [0, TI_RETURN_RING_CNT=2048) by TI_INC (if_tireg.h:965) β€” that walk index is safe. But rxidx β€” an entirely different field, the source-ring index of the buffer that was filled β€” is used raw, three times (if_ti.c:1648/1649, 1662/1663, 1676/1677):

m = sc->ti_cdata.ti_rx_jumbo_chain[rxidx];      /* array has 256 slots */
sc->ti_cdata.ti_rx_jumbo_chain[rxidx] = NULL;
...
m = sc->ti_cdata.ti_rx_mini_chain[rxidx];       /* array has 1024 slots */
sc->ti_cdata.ti_rx_mini_chain[rxidx] = NULL;
...
m = sc->ti_cdata.ti_rx_std_chain[rxidx];        /* array has 512 slots */
sc->ti_cdata.ti_rx_std_chain[rxidx] = NULL;

The driver itself only ever writes in-bounds ti_idx values into the source-ring descriptors (r->ti_idx = i at if_ti.c:714, 750, 812 with i < 256/1024/256), and well-behaved firmware echoes them. Nothing in the kernel enforces that; the field is 16 bits wide and the arrays are 512/256/1024 elements.

Why it is a real memory-safety bug (line-trace of the sink)

For rxidx β‰₯ the array size:

  1. OOB read (m = chain[rxidx], if_ti.c:1648/1662/1676): reads a kernel pointer from outside the array β€” from a neighbouring chain array, from ti_jslots[] (whose 32-byte entries contain live kernel pointers ti_sc and ti_buf, if_tireg.h:1104 + ti_jalloc use), or from past struct ti_softc (device softc in the kernel heap).
  2. OOB write (chain[rxidx] = NULL, if_ti.c:1649/1663/1677): stores 8 zero bytes at base + 8*rxidx; with a 16-bit index that is up to +511,760 bytes past the array base. run.log's table maps where each index lands inside struct ti_chain_data (if_tireg.h:1097-1105): e.g. std rxidx=512 NULLs ti_rx_jumbo_chain[0] (leaked mbuf + later NULL-deref when that slot is returned); rxidx=2047 lands in ti_jslots[] (corrupts ti_sc β†’ ti_jfree/ti_jref panic paths, if_ti.c:643-681); rxidxβ‰₯4095 writes past the softc.
  3. Wild-pointer write (if_ti.c:1690): m->m_pkthdr.len = m->m_len = cur_rx->ti_len; performs two stores at fixed small offsets of the OOB-read "mbuf" pointer, with the 16-bit store value also chosen by the NIC.
  4. Type confusion (if_ti.c:1706): ifp->if_input(ifp, m, NULL, -1) pushes the wild mbuf into ether_input/BPF β€” full type-confused traversal of attacker-influenced kernel memory.

Compare the family precedent: DF-3604/DF-3989/DF-4024/DF-4045/DF-4055 all file the same trust boundary ("NIC-DMA'd descriptor field used without a kernel-side bound"). This instance is the index variant and is strictly stronger than the length variant: it yields a write primitive (OOB NULL store + wild-pointer stores) in addition to disclosure.

Verification narrative (what I ran, what it proves)

  • trigger.c β€” a pure userland geometry model of struct ti_chain_data (if_tireg.h:1097-1105) that prints, per receive path and per sample DMA'd index, the element offset and the region the 8-byte NULL write / OOB pointer read land in. Built and run on the audit guest (build.log, run.log, RC=0, RC_DF4066_MODEL=0).
  • env.txt β€” guest is DragonFly 6.5-DEVELOPMENT X86_64_GENERIC; pciconf -l shows no Tigon-family PCI device (vendors 0x12ae Alteon, 0x10b7 3Com, 0x1385 Netgear, 0x10a9 SGI, 0x1011 DEC all absent; only virtio + i440fx). /boot/kernel/if_ti.ko exists but cannot attach, so the fault path cannot be exercised at runtime.
  • Guest /usr/src/sys/dev/netif/ti/if_ti.c md5 75c1662a... matches the audited tree β€” the audited code is exactly what ships on the guest.

Why not reproduced: no Tigon hardware exists on the guest; the bug requires a Tigon/Tigon2 NIC whose firmware emits an out-of-range descriptor index (faulty or malicious peripheral / DMA tampering). Per family convention this is recorded as inconclusive, reproduced=0, with source-level confidence certain.

Exploit chain (on real hardware, for the record)

Tigon2 with hostile/faulty firmware (or a DMA-writing attacker on the PCIe path): return-ring descriptor with TI_BDFLAG_JUMBO_RING and ti_idx = k for chosen k ∈ [256, 65535]: 1. NULL-write at &ti_cdata.ti_rx_jumbo_chain[0] + 8k β€” softc-relative, 8-byte-granular offset chosen by the descriptor. 2. OOB pointer read from the same address; the value (e.g. a ti_sc or ti_buf kernel pointer from ti_jslots, groomable via jumbo pool state) becomes m, receives two attacker-valued 16/32-bit stores at offsets +8 (m_len) and +~40 (m_pkthdr.len), then is walked by ether_input (m_next chain traversal, BPF tap read-out). NULL or non-mbuf values β†’ immediate panic (DoS floor); groomed values β†’ controlled-corruption ceiling.

Fix

fix.diff: bounds-check rxidx per receive path (>= TI_{JUMBO,MINI,STD}_RX_RING_CNT β†’ ierror + drop; the walk index has already advanced so ring progress is preserved). git apply --check clean against the read-only sys/ tree; patched module builds on the guest with -Werror (fix_build.log, FIXBUILD_f1_RC=0). Runtime fix validation impossible without hardware β†’ fix_status: not_testable.

Negative results (other paths probed in this file, cleared)

  • firmware images are compile-time #included arrays (ti_fw.h/ti_fw2.h) with static lengths β€” no runtime size validation needed (if_ti.c:402-454);
  • EEPROM reads are caller-bounded (6-byte MAC read, if_ti.c:1475);
  • ti_mem() window walk stays inside the TI_WINLEN=0x800 window (if_ti.c:378-394);
  • command-ring writes are masked by TI_INC(64) (if_ti.c:459-497);
  • TX paths index with masked frag/idx ≀ 511; the Tigon-1 banked-window mapping (idx%128 with WINBASE re-map) is in-bounds (if_ti.c:1733-1748, 1844-1861);
  • jumbo pool allocator is serializer-protected and the slot bookkeeping is consistent (if_ti.c:576-681, 256 ring slots vs 384 allocated).

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 per-path rxidx bounds checks using the driver's own drop idiom; git apply --check passes against the read-only sys/ tree and the patched if_ti.ko builds clean on the guest with -Werror -std=gnu11 (fix_build.log FIXBUILD_f1_RC=0); runtime validation impossible without Tigon hardware.

findings/poc/DF-4066/fix.diff + findings/poc/DF-4066/fix_build.log
↓ fix.diffper-fix-DF-4066

Confirmed kernel references

Detail

Exploit chain

On real (faulty/malicious) Tigon/Tigon2 hardware or via PCIe DMA tampering: return-ring descriptor with TI_BDFLAG_JUMBO_RING and chosen 16-bit ti_idx=k>=256 -> (1) 8-byte NULL write at ti_rx_jumbo_chain+8k (softc-relative, attacker-chosen granularity); (2) OOB read of the same address becomes m; (3) two attacker-valued stores at m+8/m_pkthdr.len offsets (if_ti.c:1690); (4) wild-mbuf walk through ether_input/BPF (if_ti.c:1706) -> leak or panic. Minimum guaranteed impact: NULL-deref panic (DoS); ceiling: softc-relative NULL-store corruption + type-confused mbuf traversal.

Evidence (decisive lines)

run.log (per-path geometry table: which region each DMA'd rxidx NULL-writes / OOB-reads, RC=0); build.log; env.txt (no Tigon PCI devices on guest, md5 match of audited source); VERDICT.md (full path:line trace + cleared-classes negative notes); 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 as a softc-geometry model per the DF-4024 convention; fixed an initial region-labeling off-by-array in the model (offsets now computed from struct base, not indexed-array base); honest hardware-absent verification.

Verified recommended fix

Bounds-check rxidx against TI_JUMBO_RX_RING_CNT / TI_MINI_RX_RING_CNT / TI_STD_RX_RING_CNT on the three receive paths in ti_rxeof (ierror + drop), see fix.diff.

Verdict

Source-level certain, runtime hardware-gated (inconclusive per family convention, matching DF-3604/DF-3989/DF-4024/DF-4045/DF-4055). ti_rxeof takes the receive source-ring index from the NIC-DMA'd return-ring descriptor field ti_idx (uint16_t, if_tireg.h:707/711) at if_ti.c:1638 and uses it raw to index the mbuf chain arrays -- three times, with no bounds check: jumbo path if_ti.c:1648-1649 (array TI_JUMBO_RX_RING_CNT=256), mini path if_ti.c:1662-1663 (TI_MINI_RX_RING_CNT=1024), std path if_ti.c:1676-1677 (TI_STD_RX_RING_CNT=512). An out-of-range index (any value >= array size, up to 65535) yields: (1) an OOB kernel WRITE -- chain[rxidx] = NULL stores 8 zero bytes at base+8rxidx, landing in adjacent chain arrays, ti_jslots[] (corrupting ti_sc/ti_buf used by ti_jfree/ti_jref, if_ti.c:643-681), or up to +511,760 bytes past the array outside struct ti_softc; (2) an OOB kernel READ whose result is used as struct mbuf m: if_ti.c:1690 then stores m->m_pkthdr.len = m->m_len = cur_rx->ti_len (two stores through the wild pointer, value NIC-chosen) and if_ti.c:1706 feeds m to ifp->if_input -> ether_input/BPF (wild-mbuf type confusion); NULL/garbage reads give an immediate panic (DoS floor), groomed values (e.g. ti_jslots kernel pointers) give a controlled-corruption ceiling. The driver itself only writes in-bounds r->ti_idx (if_ti.c:714/750/812), so well-behaved firmware never trips this -- the kernel simply must not trust the DMA'd field (same boundary as the DF-3604 family). Runtime: the audit guest has zero Tigon-family PCI devices (env.txt: vendors 0x12ae/0x10b7/0x1385/0x10a9/0x1011 absent, virtio-net only; if_ti.ko present but unattachable), so the fault path cannot be exercised; trigger.c (geometry model) builds and runs on the guest (run.log RC=0) proving the index-to-offset math. This is the index variant of the bce/ale/re/stge/et length-trust family and is strictly stronger: it adds an OOB write primitive.