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

Latent negative-length bcopy in fwohci_arcv split-header reassembly can cause kernel panic

Summary

In fwohci_arcv split-header reassembly path rlen recomputed as sizeof(pktbuf)-old_rlen. If old_rlen exceeds sizeof(pktbuf)=16 new rlen goes negative. Code detects with if(rlen<0) kprintf but does NOT skip subsequent bcopy which receives negative int implicitly converted to huge size_t causing massive kernel memory write and panic. Developer comment this must be too long but harmless and why rlen<0 diagnostic show awareness without understanding consequence. Currently unreachable due to buf_offset overwrite guard at line 2871 but latent landmine if control flow changes. [VERIFIED pass2: mechanically proven UNREACHABLE from packet input - battery drove every reachable header straddle, rlen always positive; the landmine is real - injected impossible state makes the very next bcopy hit the stack guard page; hardening guard validated A/B by state injection]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2303 Β· 20 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
build.log β€” 928 B view raw
build_fix.sh β€” 511 B view raw
build_kernel.sh β€” 380 B view raw
env.txt β€” 1.3 KB view raw
fix.diff β€” 596 B view raw
fwohci_harness.c β€” 22.3 KB view raw
fwohci_harness.h β€” 3.9 KB view raw
fwpoc.c β€” 7.9 KB view raw
panic.serial.txt β€” 2.7 KB view raw
panic.txt β€” 930 B view raw
prep_guest.sh β€” 1.8 KB view raw
run.log β€” 1.6 KB view raw
run.sh β€” 161 B view raw
run2303b.log β€” 337 B view raw
run2303b.sh β€” 779 B view raw
run_fixed.log β€” 3.8 KB view raw
run_fixed.sh β€” 1.9 KB view raw
verdict.json β€” 4.7 KB view raw

DF-2303 β€” latent negative-length bcopy in fwohci_arcv split-header pullup

What this is

Verification pack for finding DF-2303 (sys/bus/firewire/fwohci.c:2729-2735). Low / speculative, latent memory-corruption landmine. Claim: in the split-header reassembly path, rlen is recomputed as sizeof(pktbuf) - rlen (fwohci.c:2732); if the previous-buffer fragment exceeded sizeof(pktbuf)=16 the new rlen goes negative and the diagnostic at 2733-2734 does NOT skip the following bcopy(db_tr->buf, p, rlen) (fwohci.c:2735), whose negative int becomes a huge size_t.

Verdict (short)

NOT REPRODUCIBLE FROM PACKET INPUT β€” and the battery proves it; the landmine itself is REAL and catastrophic when the state is injected:

  • (A) Reachability battery (run.log): every header straddle the real parser can produce was driven through the REAL fwohci_arcv split-header path (firewire_debug on, "rlen=%d, offset=%d" printed by the real code at fwohci.c:2721-2723): WREQB (16B hdr): F = 4, 8, 12 -> rlen_old = 4/8/12 WRES (12B hdr): F = 4, 8 -> rlen_old = 4/8 All rlen_new = 16 - rlen_old in [4,12] > 0. No "why rlen < 0". Guest up after all runs.
  • (B) Impossible-state injection (run2303b.log, panic.txt): with buf_offset forced to -(4096-24) (rlen_old = 24 > 16 β€” a state no packet stream can produce), the real code printed "why rlen < 0" and the very next bcopy hit the stack guard page: panic: vm_fault: fault on stack guard with memmove in the trace β€” the negative-length bcopy is a live stack-smash primitive.

Why rlen_old > 16 is unreachable (line-accurate invariant)

  1. buf_offset becomes negative ONLY at fwohci.c:2762 (buf_offset = -buf_offset), immediately after fwohci_arcv_swap returned -hlen (hlen > len) for a parse at position X in buffer N (buf_offset = ld - buf = X at that point, set by fwohci.c:2800).
  2. At out: (fwohci.c:2855) if resCount != 0, line 2871 OVERWRITES buf_offset with a positive value, destroying the split state β€” so the split-header branch (2712ff) is entered only when resCount was 0, i.e. the buffer was full and len at the failing parse was psize - X = rlen_old.
  3. The parse failed with hlen > len, and hlen = tinfo[tcode].hdr_len <= 16 for every tcode arcv_swap accepts (fwohci.c:2601-2617); therefore rlen_old < hlen <= 16 whenever the state survives. Additionally every AR packet consumes a multiple of 4 bytes (12/16-byte headers + 4-byte trailer + quadlet-rounded payloads) and fwohci_arcv_swap has no STREAM case (tcode 0xa rejected, "Unknown tcode 10"), so F is quadlet-aligned: the battery set {4,8,12} is the complete reachable space for a 4096-byte buffer.

The battery mechanically confirms each element of the invariant ("split header" print, "rlen=F, offset=-X" print, clean completion).

Fix

fix.diff turns the diagnostic into a guard (drop + goto out). A/B validated: the SAME injected state on the fixed kernel prints the drop message and survives instead of panicking.

Reachability honesty

As filed: latent/unreachable today. The finding text itself says "currently unreachable ... latent landmine if control flow changes" β€” this verification upgrades that claim from speculation to mechanical proof (battery + invariant + demonstrated consequence).

VERDICT.md
↓ download raw

DF-2303 VERDICT β€” latent negative-length bcopy in fwohci_arcv

  • finding: DF-2303, fwohci.c:2729-2735 (split-header pullup, rlen = sizeof(pktbuf) - rlen; unguarded bcopy with negative len)
  • status: not_reproduced (from packet input; proven unreachable)
  • impact class: corruption (the code defect is a live stack-smash primitive β€” demonstrated by state injection β€” but no attacker- reachable input produces the state on this tree)
  • confidence: certain (as a negative result)

(A) Battery β€” the complete reachable split-header space

Rig: REAL fwohci_db_init + REAL fwohci_rx_enable on arrq (psize=4096, ndesc=1, ndb=128; per-tr buffers allocated by the real fwohci_add_rx_buf), received-DMA state fabricated exactly as OHCI AR DMA would leave it (quadlet-aligned WRES(16B)/WREQB(20B) filler chain up to X, split packet-B header at X = psize - F, resCount/status in desc.res), then REAL fwohci_arcv(sc, dbch, -1) with firewire_debug=1.

Observed from the real code (console/dmesg, run.log):

FWOH: ARCV NATURAL tcode=1 hdr=16 F=4  X=4092 -> REAL fwohci_arcv
split header
rlen=4, offset=-4092
... F=8: rlen=8, offset=-4088 ... F=12: rlen=12, offset=-4084
WRES F=4: rlen=4, offset=-4092 ... F=8: rlen=8, offset=-4088

rlen_new = 16 - rlen_old in [4,12] β€” strictly positive; reassembly completed for every case (buf_offset positive after, pdb_tr cleared); no "why rlen < 0"; guest stable after all runs.

Why this is the COMPLETE set: quadlet alignment (packet boundaries are multiples of 4) + headers of 12/16 bytes + hlen>len split criterion + the resCount==0 requirement implied by the 2871 clobber (see README) => F in {4,8,12} for 16-byte headers, {4,8} for 12-byte headers. An earlier battery variant that tried STREAM fillers proved arcv_swap rejects tcode 0xa outright ("Unknown tcode 10", fwohci.c:2614-2618) β€” STREAM never reaches the split path at all.

(B) Injection β€” the landmine is real

fwpoc ctl arcv 1 0 0 4072 sets dbch->buf_offset = -4072 (rlen_old = 24 > 16 β€” impossible per (A)) and calls the real fwohci_arcv:

FWOH: ARCV INJECT buf_offset=-4072 -> rlen_old=24 (EXCEEDS 16)
why rlen < 0                                   <- fwohci.c:2734
panic: vm_fault: fault on stack guard, addr: 0xfffff80116b51000
memmove() at memmove+0x24f <- fwoctl_ioctl (fwohci_arcv inlined)

(panic.txt) β€” first bcopy overflows pktbuf[16] on the stack, second bcopy receives (size_t)-8 and writes until the stack guard page: catastrophic kernel memory corruption IF the state is ever reachable.

Bottom line

The filed claim is exactly confirmed: the code is a latent landmine ("developer comment shows awareness without understanding consequence") that is unreachable from packet input on this tree. Recommend the one-line guard anyway (defense in depth; any future change to the 2871 clobber or to busdma resCount semantics re-arms it), plus note that the 2871 overwrite itself silently discards a legitimate split-header state (separate correctness bug, out of scope of this finding).

Fix validation (A/B)

fix.diff: if (rlen < 0) { kprintf drop; pdb_tr=NULL; buf_offset=0; goto out; }. Fixed kernel + identical injection: drop message printed, NO panic, guest up (run_fixed.log).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel: identical impossible-state injection prints fwohci_arcv: oversized header fragment, drop and survives with no panic (run_fixed.log); the natural battery behavior is unchanged (reachable path never takes the guard).

run_fixed.log DF-2303 section: CTL_ARCV mode=1 inj=4072 returns, dmesg shows oversized header fragment drop, uptime continuous
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #4: Sun Oct 18 12:45:22 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/FWO x86_64

Confirmed kernel references

Detail

Exploit chain

none from packet input (proven); state injection only: rig sets dbch->buf_offset beyond any parser-producible value -> real bcopy(db_tr->buf, p, (size_t)-8) -> stack guard page fault -> panic

Evidence (decisive lines)

run.log: battery F=4/8/12 (tcode WREQB) and F=4/8 (tcode WRES) with the real-code rlen=/offset=/split header console lines, guest up after all; run2303b.log + panic.txt: injected state -> why rlen < 0 -> panic vm_fault fault on stack guard, memmove backtrace; run_fixed.log: same injection on patched kernel prints oversized header fragment, drop and survives

PoC changes

Rig FWOIOC_ARCV: real fwohci_db_init + real fwohci_rx_enable + real fwohci_add_rx_buf allocate the AR channel; received-DMA state fabricated exactly as OHCI AR DMA leaves it (quadlet-aligned WRES(16B)/WREQB(20B) filler chain, split header at X=psize-F, resCount/status in desc.res); natural mode runs the real fwohci_arcv; inject mode forces an impossible buf_offset. First battery attempt used STREAM fillers - disproven: arcv_swap rejects tcode 0xa (Unknown tcode 10) and STREAM never reaches the split path; replaced with quadlet-aligned WRES/WREQB chains.

Verified recommended fix

Turn the rlen<0 diagnostic into a guard: drop the packet and goto out instead of falling into bcopy (fix.diff)

Verdict

The negative-length bcopy at fwohci.c:2735 is NOT reachable from packet input on this tree, and the battery proves it mechanically: every header straddle the real fwohci_arcv stream parser can produce (quadlet-aligned F in {4,8,12} for 16-byte headers, {4,8} for 12-byte headers on a 4096-byte buffer) was driven through the real split-header path with firewire_debug on - the real code printed rlen=4/8/12 with offset=-X for every case, rlen_new = 16-rlen_old stayed in [4,12], no why rlen < 0, reassembly completed, guest stable. Invariant (line-accurate): buf_offset goes negative only at fwohci.c:2762 after arcv_swap returns -hlen; if resCount!=0 the overwrite at fwohci.c:2871 discards the split state first, so the branch is only entered with resCount==0 where rlen_old = psize - X = len at the failed parse, and the parse failed precisely because hlen > len with hlen <= 16; additionally arcv_swap has no STREAM case and AR packet boundaries are quadlet multiples, so F is quadlet-aligned. rlen_old > 16 is therefore impossible. The landmine itself is real: injecting the impossible state (buf_offset=-4072, rlen_old=24) made the real code print why rlen < 0 and the very next bcopy hit the stack guard page - panic: vm_fault: fault on stack guard (memmove in trace) - a live stack-smash primitive if control flow ever changes (panic.txt). Verdict as filed: latent, unreachable today; the hardening guard is recommended and validated.