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)
PoC verification
Evidence pack
findings/poc/DF-4066 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- sys/dev/netif/ti/if_ti.c:1638
rxidx = cur_rx->ti_idx; - sys/dev/netif/ti/if_ti.c:1648-1649 jumbo:
ti_rx_jumbo_chain[rxidx]read += NULLwrite - sys/dev/netif/ti/if_ti.c:1662-1663 mini:
ti_rx_mini_chain[rxidx]read += NULLwrite - sys/dev/netif/ti/if_ti.c:1676-1677 std:
ti_rx_std_chain[rxidx]read += NULLwrite
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).
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:
- OOB read (
m = chain[rxidx], if_ti.c:1648/1662/1676): reads a kernel pointer from outside the array β from a neighbouring chain array, fromti_jslots[](whose 32-byte entries contain live kernel pointersti_scandti_buf, if_tireg.h:1104 + ti_jalloc use), or from paststruct ti_softc(device softc in the kernel heap). - OOB write (
chain[rxidx] = NULL, if_ti.c:1649/1663/1677): stores 8 zero bytes atbase + 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 insidestruct ti_chain_data(if_tireg.h:1097-1105): e.g. std rxidx=512 NULLsti_rx_jumbo_chain[0](leaked mbuf + later NULL-deref when that slot is returned); rxidx=2047 lands inti_jslots[](corruptsti_scβti_jfree/ti_jrefpanic paths, if_ti.c:643-681); rxidxβ₯4095 writes past the softc. - 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. - 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 ofstruct 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 -lshows 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.koexists but cannot attach, so the fault path cannot be exercised at runtime.- Guest
/usr/src/sys/dev/netif/ti/if_ti.cmd575c1662a...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%128with 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_testablefix.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
Confirmed kernel references
- sys/dev/netif/ti/if_ti.c:1636
- sys/dev/netif/ti/if_ti.c:1638
- sys/dev/netif/ti/if_ti.c:1648
- sys/dev/netif/ti/if_ti.c:1649
- sys/dev/netif/ti/if_ti.c:1662
- sys/dev/netif/ti/if_ti.c:1663
- sys/dev/netif/ti/if_ti.c:1676
- sys/dev/netif/ti/if_ti.c:1677
- sys/dev/netif/ti/if_ti.c:1690
- sys/dev/netif/ti/if_ti.c:1706
- sys/dev/netif/ti/if_ti.c:714
- sys/dev/netif/ti/if_ti.c:750
- sys/dev/netif/ti/if_ti.c:812
- sys/dev/netif/ti/if_tireg.h:707
- sys/dev/netif/ti/if_tireg.h:711
- sys/dev/netif/ti/if_tireg.h:397
- sys/dev/netif/ti/if_tireg.h:399
- sys/dev/netif/ti/if_tireg.h:1099
- sys/dev/netif/ti/if_tireg.h:1102
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.
No comments yet.