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

sf_txeof() trusts DMA-written 15-bit TX completion index: OOB descriptor read feeds arbitrary pointer to m_freem(), OOB NULL write, and sf_tx_cnt underflow

Summary

sf_txeof computes the descriptor address from the 15-bit DMA-written sf_index with no bounds check - the derived index can be 128..255 against a 128-entry ring. The driver then reads sf_mbuf OOB (into the RX completion region), calls m_freem() on that garbage pointer, NULL-writes it, and decrements sf_tx_cnt into underflow (defeating the ring-full checks). PROVEN on the guest with a kld harness: gcc -Warray-bounds statically rejects the driver expression; runtime mode 1 shows the OOB read/NULL-write/underflow; mode 2 executes the real m_freem(0xdeadc0de0000) -> Fatal trap 9 at m_free+0x10. Arbitrary-free primitive from a peripheral trust source. Bounds-check + resync fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4325 Β· 12 files
FileTypeDescriptionSize
Makefile β€” 147 B ↓ download
README.md β€” 1.4 KB ↓ raw
VERDICT.md β€” 2.7 KB ↓ raw
build.log β€” 1.5 KB view raw
build.sh β€” 165 B view raw
env.txt β€” 457 B view raw
fix.diff β€” 1.2 KB view raw
panic.txt β€” 1.3 KB view raw
run.log β€” 3.0 KB view raw
run.sh β€” 177 B view raw
sf_ring_harness.c β€” 10.5 KB view raw
verdict.json β€” 2.8 KB view raw

DF-4325 β€” sf(4) TX completion index OOB (arbitrary m_freem)

File: sys/dev/netif/sf/if_sf.c:998-1013 (sf_txeof)

cur_cmp->sf_index is a 15-bit byte-offset DMA-written by the AIC-6915 into the TX completion queue. The driver does cur_tx = &sf_tx_dlist[sf_index >> 7] with no bounds check against SF_TX_DLIST_CNT (128). Values >= 16384 (possible up to 32767) index entries 128..255 β€” 16..32KB from the ring base β€” reading an sf_mbuf "pointer" from unrelated ring memory and feeding it to m_freem(), NULL-writing it, and decrementing sf_tx_cnt below zero.

Build (guest with /usr/src)

cp /usr/src/sys/dev/netif/sf/if_sfreg.h /root/kmod/sfring/
cd /root/kmod/sfring && make SYSDIR=/usr/src/sys
kldload ./sf_ring_harness.ko

Run / expected

sysctl kern.sfrh.run=1   # log-only: OOB read value, NULL write, tx_cnt -1
sysctl kern.sfrh.run=2   # REAL m_freem(0xdeadc0de0000) -> kernel panic

Expected (mode 2, captured in panic.txt): sfrh[tx]: mode 2: calling REAL m_freem(0xdeadc0de0000) ... Fatal trap 9: general protection fault while in kernel mode Stopped at m_free+0x10: movswq 0x20(%rdi),%rax

gcc itself proves the index OOB at build time (-Warray-bounds, build.log).

No sf(4) hardware exists on the audit guest; the harness reproduces the exact descriptor-processing code path with DMA-provided values planted directly.

VERDICT.md
↓ download raw

DF-4325 β€” verdict: REPRODUCED (harness on live kernel; hardware-gated path)

Root cause (path:line)

  • sys/dev/netif/sf/if_sf.c:998 β€” cur_tx = &sc->sf_ldata->sf_tx_dlist[cur_cmp->sf_index >> 7]; sf_index is u_int32_t sf_index:15 (if_sfreg.h:899-904), i.e. 0..32767; >> 7 yields 0..255; sf_tx_dlist has SF_TX_DLIST_CNT = 128 entries (if_sfreg.h:781). The value is DMA-written by the NIC (completion queue).
  • if_sf.c:1008-1013 β€” the OOB-read cur_tx->sf_mbuf is passed to m_freem(), then NULL-written; sc->sf_tx_cnt-- per iteration can underflow to negative.

What was proven on the guest

  1. Static proof: gcc -Werror=array-bounds rejects the harness build with the literal driver expressions: "array subscript 255 is above array bounds of 'struct sf_tx_bufdesc_type0[128]'" (build.log). The driver compiles only because its index is a runtime DMA value.
  2. Runtime OOB read+write+underflow (kern.sfrh.run=1, run.log): sf_index=0x7ff8 -> idx 255 -> OOB READ at +16256 past the ring (inside the sf_rx_clist region): reads planted 0xdeadc0de0000; OOB NULL WRITE at the same offset; sf_tx_cnt 0 -> -1.
  3. Wild-free primitive (kern.sfrh.run=2, panic.txt): REAL m_freem(0xdeadc0de0000) -> "Fatal trap 9: general protection fault", "Stopped at m_free+0x10: movswq 0x20(%rdi),%rax" β€” the kernel dereferenced the OOB-read value exactly as if_sf.c:1010 would.

Impact

Memory-corruption / arbitrary-free primitive gated on NIC-supplied completion data (compromised or faulty AIC-6915, or a PCIe/DMA fault): OOB pointer free (wild m_freem), NULL-poisoning of RX ring memory, and TX accounting underflow that later defeats the ring-full check (if_sf.c:1274). On hardware this is a kernel memory-corruption bug; realistic ceiling is host crash to heap corruption depending on adjacent allocations.

Why not uid=0: the trigger requires the sf(4) NIC itself (or its DMA) to emit the malicious descriptor; no such hardware exists on the audit guest, so the escalation chain could not be attempted beyond the demonstrated primitive.

Fix

Bounds-check the derived index before use and resync (fix.diff).

Fix validation

fix_status=not_testable: the fix lives in sf_txeof(), which only executes on AIC-6915 hardware the guest does not have; the harness replicates the driver code in its own module and therefore cannot observe a sys/ source change.

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 reproduced→ patch + rebuild →patched clean

fix targets sf_txeof() which only executes on AIC-6915 hardware absent from the guest; the harness replicates driver code inside its own module and cannot observe a sys/ source change

fix.diff authored (bounds check + resync); no hardware path to validate
↓ fix.diffper-fix-DF-4325

Confirmed kernel references

Detail

Exploit chain

on-hw chain (not exercisable on guest): NIC/faulty DMA emits TX completion with sf_index>=16384 -> OOB pointer read -> m_freem(arbitrary value) = wild free / NULL-write into RX rings + tx_cnt underflow -> heap corruption -> crash..privesc depending on adjacent allocations

Evidence (decisive lines)

run.log (mode1 log lines + mode2 panic sequence), panic.txt (Fatal trap 9, Stopped at m_free+0x10), build.log (gcc array-bounds static proof), sf_ring_harness.c

PoC changes

harness authored from scratch (none seeded): replicates sf_txeof index math + m_freem/NULL-write/tx_cnt stores verbatim; volatile laundering added so gcc cannot constant-fold the deliberately-OOB index (its -Warray-bounds diagnostic kept as static evidence)

Verified recommended fix

bounds-check cur_cmp->sf_index>>7 against SF_TX_DLIST_CNT and resync/reset on violation

Verdict

REPRODUCED on the live guest kernel via harness: sf_txeof's unchecked DMA-supplied 15-bit completion index (sf_index>>7, if_sf.c:998) indexes the 128-entry TX ring with values up to 255; the OOB-read 'sf_mbuf' (planted 0xdeadc0de0000) fed to the REAL m_freem() panicked the kernel in m_free+0x10 (GPF), plus OOB NULL write and sf_tx_cnt 0->-1 underflow logged. gcc -Warray-bounds independently proves the expression OOB at build time. Hardware-gated in production (needs AIC-6915 completion DMA); uid=0 chain not attempted: no sf NIC on guest.