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

FW_ASYREQ send-path bcopy over-reads ioctl heap buffer when req.len exceeds data[] capacity

Summary

In FW_ASYREQ send path pay_len=MAX(0 req.len-hdr_len) bounded only by 16-bit req.len field never by sizeof(asyreq->data). bcopy reads pay_len bytes from &asyreq->data[0] (2048 bytes) so any req.len>2064 causes kernel heap over-read into adjacent M_IOCTLOPS object. On typical S400 hardware MAXREC(fc->maxrec)=512 causes fw_asyreq to reject xfer before transmission so over-read data stays in xfer->send.payload and freed - but bcopy has already executed. On faster controllers or large maxrec leaked heap can be transmitted to FireWire peer. [VERIFIED pass2: reproduced - req.len 4096 executes 4080-byte bcopy 2032 past the 2084-byte M_IOCTLOPS staging buffer; S400 gate fires AFTER the bcopy, S800 completes to the wire with over-read bytes in the TX payload; response-path write variant corrupts 240 bytes past; SIBLING BUG filed separately - destination typo to address-of-pointer at fwdev.c 561 is a privesc-class heap overwrite]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2296 Β· 28 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 7.5 KB ↓ raw
build.log β€” 211 B view raw
build.sh β€” 111 B view raw
build_kernel.sh β€” 396 B view raw
dmesg_baseline_phases.txt β€” 767 B view raw
dmesg_phaseA_gate.txt β€” 900 B view raw
dmesg_run.txt β€” 0 B ↓ download
dmesg_serial_run.txt β€” 2.9 KB view raw
env.txt β€” 466 B view raw
fix.diff β€” 1.6 KB view raw
fix_run.log β€” 928 B view raw
fwharness.c β€” 18.8 KB view raw
fwharness.h β€” 1.4 KB view raw
fwhctl.c β€” 3.7 KB view raw
poc_2296.c β€” 6.0 KB view raw
prep_guest.sh β€” 1.2 KB view raw
run.legit_baseline.log β€” 443 B view raw
run.sh β€” 90 B view raw
run_2296.log β€” 3.5 KB view raw
run_2296.sh β€” 2.2 KB view raw
smash_panic_gfp_kfree.txt β€” 4.9 KB view raw
spraydump.A1.txt β€” 10.7 KB view raw
spraydump.A2.txt β€” 10.7 KB view raw
spraydump.B1.txt β€” 10.7 KB view raw
spraydump.B2.txt β€” 10.8 KB view raw
spraydump.C.txt β€” 10.3 KB view raw
verdict.json β€” 4.5 KB view raw

DF-2296 β€” FW_ASYREQ unbounded pay_len bcopy over-read (+ discovered sibling write bug)

Verdict (short)

REPRODUCED on the stock fwdev.o/firewire.o binaries β€” core claim confirmed; plus a worse sibling bug was discovered on the same line (see Sibling finding below β€” flagged for a new DF-ID by the orchestrator).

Claim proven: * pay_len = MAX(0, asyreq->req.len - tinfo->hdr_len) (fwdev.c:530) is never bounded by sizeof(asyreq->data) (2048 bytes). With req.len 4096 (u16 max 65535), the bcopy at fwdev.c:560 copies 4080 bytes from asyreq+36 β€” 2032 bytes past the 2084-byte M_IOCTLOPS staging buffer (sys_generic.c:675) β€” into the xfer payload area. * S400 ordering proven: with fc->maxrec=10 (fwohci.c:521: fc->maxrec = fc->speed + 8, S400 speed 2 β†’ MAXREC 2048 β€” note: the original claim said 512; actual stock S400 gate is 2048 quadlet bytes), fw_asyreq rejects with send.pay_len > maxrec (firewire.c:210) and the ioctl returns EINVAL β€” but the bcopy at fwdev.c:560 runs unconditionally BEFORE fw_asyreq (fwdev.c:565), dmesg confirms the gate line after the copy executed. * Full path on big-maxrec: with fc->maxrec=11 (S800 speed 3 β†’ MAXREC 4096), the same request completes the entire stock path β€” fw_asyreq β†’ fw_asystart β†’ the interposed OHCI TX start records a 4080- byte "wire" payload snapshot. On real hardware those bytes (including the 2032 bytes read past the ioctl buffer) are DMA'd onto the bus. fwohci max maxrec is 11 (speed+8, MAX_SPEED 3) β†’ wire disclosure up to 2048 bytes of adjacent kernel heap on S800-capable controllers.

Fix validated: pay_len > sizeof(asyreq->data) β†’ EINVAL (before any alloc/copy); legit req.len=2064 (pay_len 2048 == capacity) still runs the full path identically (rc=0, wire snapshot 2048 bytes).

Sibling finding discovered during verification (NEW β€” needs an ID)

fwdev.c:561: bcopy(..., (void *)&xfer->send.payload, pay_len); β€” copies into the ADDRESS OF the payload pointer field, not the payload buffer (FreeBSD's original is (void *)xfer->send.payload). For ANY FW_ASYREQ block request this smashes the tail of struct fw_xfer (send.payload/pay_len/spd, recv.hdr/payload/pay_len, mbuf, link, malloc β€” offsets published by the rig: send.payload=+80 … malloc=+144, sizeof=152) and overruns the 160-class fw_xfer allocation by up to ~63KB with fully user-controlled packet bytes (a kernel heap overwrite primitive reachable by the same operator-group ioctl, no peer needed). Three distinct panics were captured while walking this path stock (GPF in _kfree freeing the smashed send/recv.payload, page fault in pmap_kvtom), and behavioral control of the smashed fields was demonstrated (planted recv.hdr tcode steered the response path; planted lenval steered the MAXREC gate). One-character fix: drop the &. See smash_panic_gfp_kfree.txt / dmesg_phaseA_gate.txt / VERDICT.md.

Files

  • poc_2296.c β€” trigger with stock-compatible field planting (keeps the smashed fw_xfer tail valid so the full stock path completes)
  • run_2296.sh + run_2296.log β€” A (S400 reject-after-copy), B (S800 full path), C (response-path write variant) + spray canary analysis
  • dmesg_phaseA_gate.txt, dmesg_serial_run.txt, smash_panic_gfp_kfree.txt
  • spraydump.{A1,A2,B1,B2,C}.txt β€” canary dumps
  • run.legit_baseline.log, fix_run.log β€” A/B evidence
  • fix.diff β€” combined fix (this finding: pay_len bound)
VERDICT.md
↓ download raw

DF-2296 VERDICT β€” REPRODUCED (kernel heap over-read into TX payload; wire disclosure on S800)

Claim and code (line-accurate)

fw_ioctl FW_ASYREQ (fwdev.c:521-587):
526:    fp = &asyreq->pkt;                     /* offset 20 in struct */
527:    tinfo = &sc->fc->tcode[fp->mode.hdr.tcode];
529:    if ((tinfo->flag & FWTI_BLOCK_ASY) != 0)
530:        pay_len = MAX(0, asyreq->req.len - tinfo->hdr_len);
     /* req.len: unsigned short β€” NEVER bounded by sizeof(asyreq->data)
      * (2048 bytes at struct offset 36; struct is 2084 bytes) */
532:    xfer = fw_xfer_alloc_buf(M_FWXFER, pay_len, PAGE_SIZE);
558:    bcopy(fp, (void *)&xfer->send.hdr, tinfo->hdr_len);
559:    if (pay_len > 0)
560:        bcopy((char *)fp + tinfo->hdr_len,
561:            (void *)&xfer->send.payload, pay_len);
     /* SOURCE: asyreq+20+16 = asyreq+36 = data[0]; pay_len up to 65519
      * reads up to ~63KB past the 2084-byte ioctl staging buffer
      * (kmalloc(IOCPARM_LEN, M_IOCTLOPS), sys/kern/sys_generic.c:675)
      * into adjacent kernel heap */
565:    if ((err = fw_asyreq(sc->fc, -1, xfer)) != 0)   /* MAXREC gate is
      * INSIDE fw_asyreq (firewire.c:210), i.e. AFTER the bcopy */

fw_asyreq (firewire.c:183-250):

210:    if (xfer->send.pay_len > MAXREC(fc->maxrec)) {
211:        kprintf("send.pay_len > maxrec\n");
212:        return EINVAL;

MAXREC(x) = 2 << x (firewire.h:73); stock controllers set fc->maxrec = fc->speed + 8 (fwohci.c:521), so S100/200/400/800 β†’ 512/1024/2048/4096 bytes. (Correction to the original finding text: the S400 gate is 2048 bytes, not 512.)

Observed (baseline harness kernel, stock fwdev.o/firewire.o)

The rig plants field values into data[] that keep the (smashed, see sibling) fw_xfer tail valid so the unmodified stock path completes; see poc_2296.c header for the exact recipe and rig sysctls hw.fwh_off_* (exact kernel offsetof values).

  • Phase A β€” S400 (maxrec=10), req.len=4096 (pay_len=4080): send.pay_len > maxrec printed (firewire.c:210), ioctl returns EINVAL β€” and this rejection is only reachable at fwdev.c:565, i.e. strictly AFTER the 4080-byte bcopy at :560-561 already executed (dmesg_phaseA_gate.txt, run_2296.log).
  • Phase B β€” S800 (maxrec=11), same request: full stock path completes (rc=0): fw_asyreq β†’ fw_get_tlabel β†’ fw_asystart β†’ the interposed OHCI AT-start: fwh: AT-START: 'wire' payload snapshot 4080 bytes (wire count=1). The payload that real hardware would DMA onto the FireWire bus contains, in its last 2032 bytes, whatever kernel heap followed the ioctl staging buffer. The staging buffer is exactly 2084 bytes; data[] ends at the buffer end; everything the bcopy read beyond index 2048 is adjacent kernel memory.
  • Phase C β€” response-path write variant (supplementary): with an emulated 2304-byte peer block response, the response copies at fwdev.c:582-584 (bcopy(xfer->recv.payload, (char*)fp + hdr_len, MAX(0, req.len - hdr_len)) β€” note the EINVAL branch at :581 does NOT skip these copies) write req.len-16 bytes into the staging buffer at pkt+16 β€” 240 bytes past its end for recvpl=2304. Executed (rc=0) with canary-corruption detected (SPRAYCHK corrupt=1, badwords=576, dmesg_serial_run.txt); byte-attribution of the 240 bytes to a specific canary chunk could not be pinned down because the staging buffer's slab placement never landed adjacent to a monitored chunk within the runs performed (spraydump.C.txt shows monitored 2084-chunks intact; the corruption landed in the 160-class zone).
  • Canary-attribution caveat (honest): the slab allocator's per-CPU magazines prevented deterministic placement of the fw_xfer next to sprayed 160-byte canaries (SPRAYCHK corrupt=0 for phases A/B in the final run; earlier runs showed corrupt=1 from the destination-side smash). The READ past the 2084-byte staging buffer is nonetheless proven: the copy length (4080) is twice the data[] capacity, the copy demonstrably executed (phase B full path; phase A gate order), and the source pointer (asyreq+36) is arithmetically fixed.

Sibling finding discovered (needs a new DF-ID) β€” worse than DF-2296

fwdev.c:561 copies to (void *)&xfer->send.payload β€” the address of the pointer field (DragonFly transcription bug; FreeBSD uses (void *)xfer->send.payload). Effects measured on the stock binary: 1. For any block FW_ASYREQ, 8..~72 bytes of struct fw_xfer after send.payload are overwritten with packet bytes (rig published offsets: send.payload=+80, recv.hdr=+96, recv.payload=+112, recv.pay_len=+120, mbuf=+128, malloc=+144; sizeof=152), and for pay_len>72 the copy overruns the 160-class fw_xfer allocation with attacker-controlled bytes β€” up to ~63KB (u16 req.len). 2. The smashed send.payload/recv.payload are then kfree()d by fw_xfer_free_buf (fwdev.c:586): captured stock panics include Fatal trap 9: general protection fault in _kfree ← fw_xfer_free_buf ← fw_ioctl (twice) and Fatal trap 12 in pmap_kvtom ← fw_xfer_free_buf (smash_panic_gfp_kfree.txt, dmesg_phaseA_gate.txt). 3. Behavioral control proven: planting recv.hdr tcode=2 in data[] steered the response path to the early-out; tcode=1 + recv.pay_len steered it into the response-copy branch; planting lenval steered the MAXREC gate. This is a local, operator-group kernel heap overwrite with fully controlled content and length β€” privesc-class. Recommended one-line fix: s/(void *)&xfer->send.payload/(void *)xfer->send.payload/ (NULL- or alloc-guard the payload pointer as appropriate).

Threat model / reachability (honest)

  • Trigger: operator-group user, one ioctl (FW_ASYREQ, block tcode).
  • Disclosure sink on stock fwohci: the over-read bytes are copied into xfer->send.payload; when pay_len <= MAXREC(fc->maxrec) they are transmitted to the FireWire peer (wire disclosure, up to 2048 bytes of adjacent heap per request on S800 hardware, maxrec 11). On S400 (typical, maxrec 10 β†’ 2048 == data capacity) the gate rejects before transmission β€” but AFTER the bcopy; the over-read data then sits in the payload buffer until freed (no local read-back path exists via this ioctl on stock). Local user cannot read the over-read bytes directly; the sibling &payload bug (above) turns the same ioctl into a local heap overwrite instead.
  • Privesc assessment: DF-2296 itself = remote-peer disclosure (leak); no local escalation. The sibling bug = local privesc-class heap overwrite, flagged.

Fix validation (A/B, fix.diff pay_len bound)

        if (pay_len > sizeof(asyreq->data)) {
            err = EINVAL;
            break;
        }

inserted after fwdev.c:530 (before fw_xfer_alloc_buf β€” no leak, no copy). Patched kernel (FWH #8): * malicious req.len=4096 β†’ ioctl rc=-1 errno=22, NO wire transmit (hw.fwh_txcount unchanged by the malicious request); * legit req.len=2064 (pay_len 2048 == data[] capacity) β†’ rc=0, full path, AT-START: 'wire' payload snapshot 2048 bytes β€” identical to the baseline-kernel behavior of the same request (run.legit_baseline.log, fix_run.log); * guest alive after both.

Artifacts

run_2296.log (phases + dmesg), dmesg_phaseA_gate.txt, dmesg_serial_run.txt, smash_panic_gfp_kfree.txt, spraydump.*.txt, run.legit_baseline.log, fix_run.log, poc_2296.c, fwharness.c/fwhctl.c (rig), env.txt.

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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #8: Sun Oct 18 07:38:56 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/FWH x86_64

Confirmed kernel references

Detail

Exploit chain

operator-group FW_ASYREQ ioctl with req.len>2064: bcopy over-reads up to ~63KB past the ioctl staging buffer into the TX payload (transmitted to a FireWire peer on maxrec>=11 hardware; rejected after the copy on S400). Locally the bytes are not readable back on stock; the sibling &payload destination bug turns the same ioctl into a local heap OVERWRITE (privesc-class, flagged separately).

Evidence (decisive lines)

run_2296.log (phase A dmesg send.pay_len > maxrec, phase B AT-START wire 4080, phase C SPRAYCHK corrupt=1), dmesg_phaseA_gate.txt, dmesg_serial_run.txt, smash_panic_gfp_kfree.txt (stock GPF _kfree <- fw_xfer_free_buf <- fw_ioctl), spraydump.*.txt, run.legit_baseline.log + fix_run.log (A/B: 4096 EINVAL no-tx, 2064 full path identical)

PoC changes

Substantial: seed sketched a plain ioctl; the stock kernel proved un-runnable without handling the sibling &payload smash (three panics), so the PoC plants valid values into the fw_xfer fields the smash overwrites (offsets taken from rig-published kernel offsetof sysctls) to walk the full stock path; phases A/B/C added; rresb.len byte order fixed (high 16 bits).

Verified recommended fix

Bound pay_len by sizeof(asyreq->data) -> EINVAL before fw_xfer_alloc_buf. Sibling (new finding): change (void )&xfer->send.payload to (void )xfer->send.payload at fwdev.c:561.

Verdict

pay_len = MAX(0, req.len - hdr_len) (fwdev.c:530) is never bounded by sizeof(asyreq->data)=2048; the bcopy at fwdev.c:560-561 reads pay_len bytes from the 2084-byte M_IOCTLOPS staging buffer (sys_generic.c:675). Proven on the stock binaries: req.len=4096 executes a 4080-byte copy (2032 bytes past the buffer); on S400 (fc->maxrec=10 per fwohci.c:521, gate=2048 - the original claim said 512, corrected) fw_asyreq rejects with send.pay_len > maxrec AFTER the bcopy already ran; on S800 (maxrec=11) the identical request completes the full path and the interposed TX start records a 4080-byte wire payload (the over-read bytes would be transmitted to the peer on hardware). The response-path variant (fwdev.c:582-584, executed with an emulated 2304-byte peer response) writes req.len-16 bytes into the staging buffer, 240 bytes past its end (canary corruption detected; chunk-level attribution inconclusive due to slab placement). SIBLING BUG DISCOVERED at the same line: the destination is (void *)&xfer->send.payload - the address of the pointer field - smashing the fw_xfer tail and overrunning the 160-class allocation with fully user-controlled bytes (three stock panics captured in _kfree/pmap_kvtom; behavioral control of smashed fields demonstrated). That sibling is a privesc-class local heap overwrite and is flagged for a new DF-ID. Fix for DF-2296 validated: pay_len bound returns EINVAL for 4096 with no transmit; legit 2064/2048 runs the full stock path identically.