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

ti_rxeof trusts the NIC-DMA'd 16-bit return-descriptor ti_len with no bound vs the backing buffer: std-ring m_len up to 65535 on a 2048-byte cluster (63489-byte heap OOB read into delivered frames), jumbo up to 56513 bytes past a 9024-byte slot β€” DF-3604/3989/4024/4045/4055 family, ti(4) instance

Summary

The delivered mbuf's length is assigned verbatim from the descriptor's 16-bit ti_len and handed to if_input with no comparison against the buffer actually backing the ring (std 2046 usable / mini MHLEN-2 / jumbo 9022). A lying descriptor makes every m_len consumer read past the buffer: adjacent-cluster/pool kernel heap disclosed into received packets (readable by any local user with a socket on the interface) or unmapped-tail fault. Composes with DF-4066: one descriptor picks the buffer AND declares its length. Guest-executed extents model; hardware-gated. Per-ring maxlen drop fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4067 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
trigger.c β€” 3.9 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 163 B view raw
build.log β€” 464 B view raw
run.log β€” 813 B view raw
env.txt β€” 977 B view raw
fix.diff β€” 1.4 KB view raw
fix_build.log β€” 998 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 5.0 KB view raw

DF-4067 β€” ti_rxeof trusts NIC-DMA'd 16-bit frame length

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

After detaching the received buffer, ti_rxeof sets the delivered mbuf's length straight from the return-ring descriptor's 16-bit ti_len field with no bound against the buffer actually backing it:

sys/dev/netif/ti/if_ti.c:1690
    m->m_pkthdr.len = m->m_len = cur_rx->ti_len;

Buffers per ring: std : m_getcl() cluster = MCLBYTES 2048 (sys/sys/param.h:497), m_adj(ETHER_ALIGN=2) -> 2046 usable (if_ti.c:694-705); firmware-legit ceiling is rcb->ti_max_len = TI_FRAMELEN 1518 (if_ti.c:1279, if_tireg.h:821). mini : MGETHDR data area MHLEN, m_adj(2) (if_ti.c:730-741); rcb max_len = MHLEN-2 (if_ti.c:1305). jumbo : driver jumbo slot TI_JLEN = roundup(9018+2,8) = 9024, data at +2 -> 9022 usable (if_ti.c:799-802; if_tireg.h:822,1048-1050).

A descriptor claiming ti_len beyond the buffer (up to 65535) makes the mbuf advertise bytes it does not own; every m_len/pkthdr.len consumer (BPF tap, ether_input, protocol m_copydata/pullup, socket receive) reads past the buffer: kernel-heap disclosure into delivered frames or a page fault on an unmapped tail. std path worst case: m_len=65535 on a 2048-byte cluster = 63489 bytes OOB (16x the cluster).

ti(4) instance of the DF-3604 (bce) / DF-3989 (ale) / DF-4024 (re) / DF-4045 (stge) / DF-4055 (et) length-trust family.

Build / run (audit guest)

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

Expected: per-ring OOB extent table (RC_DF4067_MODEL=0). The model does NOT touch kernel memory; the real path needs Tigon hardware writing a lying ti_len into the DMA'd return ring.

Hardware gate (why status=inconclusive)

env.txt: audit guest has no Tigon-family PCI device (vendors 0x12ae/0x10b7/0x1385/0x10a9/0x1011 absent); if_ti.ko cannot attach. Family convention (DF-3604/3989/4024/4045 = inconclusive).

Fix (fix.diff, compile-validated)

Per-ring maxlen (MCLBYTES-2 / MHLEN-2 / TI_JUMBO_FRAMELEN-2); if the descriptor's ti_len exceeds it, count an ierror and free the mbuf instead of delivering it. git apply --check clean; patched module builds on the guest with -Werror (fix_build.log: FIXBUILD_f2_RC=0). Runtime fix validation not possible without hardware (fix_status=not_testable).

VERDICT.md
↓ download raw

DF-4067 β€” VERDICT

Status: inconclusive (hardware-gated) Β· source-level: certain Β· severity: Medium Β· bucket: kernleak

Root cause

ti_rxeof() (sys/dev/netif/ti/if_ti.c:1623-1717) hands the received frame up with its length taken verbatim from the NIC-DMA'd return-ring descriptor:

m->m_pkthdr.len = m->m_len = cur_rx->ti_len;      /* if_ti.c:1690 */
...
ifp->if_input(ifp, m, NULL, -1);                  /* if_ti.c:1706 */

ti_len is a plain uint16_t (if_tireg.h:710) β€” the Tigon firmware can write any value 0..65535. The driver never compares it with the capacity of the buffer that backs the descriptor:

ring buffer usable firmware-legit max (rcb->ti_max_len)
std m_getcl cluster MCLBYTES(2048) βˆ’ ETHER_ALIGN(2) = 2046 TI_FRAMELEN 1518 (if_ti.c:1279)
mini MGETHDR data area MHLEN βˆ’ 2 MHLEN βˆ’ 2 (if_ti.c:1305)
jumbo driver pool slot TI_JLEN 9024 9022 TI_JUMBO_FRAMELEN 9018 (if_ti.c:1290)

So even a legit firmware is trusted to respect max_len; a faulty or malicious one (or DMA tampering on the PCIe path) can claim up to 65535 bytes. Result: an mbuf whose m_len/m_pkthdr.len exceed its storage. Every consumer that trusts those fields then reads past the buffer:

  • ETHER_BPF_MTAP/BPF read-out of the frame,
  • ether_input β†’ m_pullup/m_copydata in the protocol stack,
  • socket receive copies

β†’ kernel-heap contents (adjacent clusters, adjacent mbufs, jumbo pool neighbours β€” and past the contigmalloc block for large claims on the jumbo ring) are copied into delivered packets β€” a kernel info leak to any local user with a receiving socket on the interface β€” or the walk faults on an unmapped tail (panic). Worst case std path: 65535 bytes claimed on a 2048-byte cluster = 63489 bytes OOB read (run.log).

Why inconclusive at runtime, certain at source

Same boundary as DF-3604/DF-3989/DF-4024/DF-4045/DF-4055 (all inconclusive/certain): the audit guest has no Tigon hardware (env.txt: pciconf -l shows only virtio + i440fx; Tigon vendors 0x12ae/0x10b7/0x1385/0x10a9/0x1011 absent), so the trigger β€” a firmware that lies in ti_len β€” cannot be instantiated. The source facts are not in dispute: there is no clamp anywhere between the descriptor read (if_ti.c:1636-1638) and the length assignment (if_ti.c:1690), verified by reading every statement of ti_rxeof; trigger.c models the per-ring extents and runs on the guest (run.log, RC=0).

Note the sibling interaction: DF-4066 (ti_idx) and this bug compose β€” a single descriptor both picks the buffer and declares its length.

Verification narrative

  • trigger.c built+run on guest: prints per-ring usable/legit/claimable extents and the two headline numbers (std 63489 B OOB; jumbo 56513 B OOB past a 9024-byte slot, slot pool 3,465,216 B so large claims run past the contigmalloc region). RC_DF4067_MODEL=0.
  • env.txt: guest DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, no Tigon devices; guest /usr/src if_ti.c md5 75c1662a... == audited tree.

Fix

fix.diff: introduce per-ring maxlen (MCLBYTESβˆ’2 / MHLENβˆ’2 / TI_JUMBO_FRAMELENβˆ’2) and drop (ierror + m_freem) any frame whose DMA'd ti_len exceeds it. The mbuf was already replaced in the ring by ti_newbuf_*, so freeing it is the same as the existing TI_BDFLAG_ERROR path. git apply --check clean; patched module builds on the guest with -Werror (fix_build.log FIXBUILD_f2_RC=0); runtime validation needs hardware (fix_status=not_testable).

Cleared in this pass (adjacent length surfaces)

  • TX lengths go mbuf β†’ descriptor (f->ti_len = m->m_len, if_ti.c:1865); mbuf lengths are kernel-produced, no device input.
  • ti_newbuf_* buffer/descriptor pairs are consistent (descriptors armed with exactly the buffer capacity, if_ti.c:707-716/742-751/ 803-813).
  • The mini-ring rcb->ti_max_len = MHLEN - ETHER_ALIGN (if_ti.c:1305) matches the buffer; only the return-path trust is missing.

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 a per-ring maxlen and drops over-length frames using the driver's existing error-drop idiom; git apply --check passes and the patched if_ti.ko builds clean on the guest with -Werror -std=gnu11 (fix_build.log FIXBUILD_f2_RC=0); runtime validation impossible without Tigon hardware.

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

Confirmed kernel references

Detail

Exploit chain

On real (faulty/malicious) Tigon/Tigon2 hardware or PCIe DMA tampering: return-ring descriptor with no TI_BDFLAG_ERROR and ti_len=65535 -> driver builds an mbuf claiming 65535 bytes over a 2048-byte std cluster -> leaked kernel-heap bytes ride into received frames, readable by any local user with a receiving socket on tiX / a BPF listener, or the stack faults on an unmapped tail -> panic. Inbound-only: read primitive (plus DF-4066's write primitive on the same descriptor).

Evidence (decisive lines)

run.log (per-ring extent table, std 63489 B / jumbo 56513 B headline numbers, RC=0); build.log; env.txt (no Tigon PCI devices on guest, md5 match of audited source); VERDICT.md (path:line trace + cleared length 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 (per-ring extent model) per the DF-4024 convention; honest hardware-absent verification.

Verified recommended fix

Bound cur_rx->ti_len by the ring's buffer capacity (MCLBYTES-2 / MHLEN-2 / TI_JUMBO_FRAMELEN-2) in ti_rxeof; drop over-length frames (ierror + m_freem), 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 sets the delivered mbuf's length verbatim from the NIC-DMA'd return-ring descriptor field ti_len (uint16_t, if_tireg.h:710) at if_ti.c:1690 (m->m_pkthdr.len = m->m_len = cur_rx->ti_len) and hands the mbuf to ifp->if_input at if_ti.c:1706 -- with NO bound against the buffer actually backing the descriptor: std ring cluster = MCLBYTES 2048 minus ETHER_ALIGN 2 = 2046 usable (if_ti.c:694-705, armed in ti_newbuf_std; firmware-legit ceiling rcb->ti_max_len = TI_FRAMELEN 1518 at if_ti.c:1279), mini ring MHLEN-2 (if_ti.c:730-741,1305), jumbo ring TI_JLEN slot 9024 minus 2 = 9022 usable (if_ti.c:799-802, rcb max 9018 at if_ti.c:1290). A descriptor claiming ti_len above the buffer capacity makes every m_len consumer (BPF tap, ether_input, protocol m_copydata/pullup, socket receive) read past the buffer: adjacent-cluster/mbuf/pool kernel heap disclosed into delivered packets (info leak to any local user with a socket on the interface) or unmapped-tail fault (panic). Worst cases: std m_len=65535 on 2048B cluster = 63489 bytes OOB (16x the cluster); jumbo 65535 on 9024B slot = 56513 bytes OOB, past the 384*9024=3.4MB contigmalloc pool for large claims. Composes with DF-4066 (same descriptor picks the buffer AND declares its length). Runtime: audit guest has zero Tigon-family PCI devices (env.txt), so the faulty-firmware trigger cannot be instantiated; trigger.c (extent model) builds and runs on the guest (run.log RC=0).