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)
PoC verification
Evidence pack
findings/poc/DF-2296 Β· 28 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
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 > maxrecprinted (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) writereq.len-16bytes 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; whenpay_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
fixedConfirmed kernel references
- sys/bus/firewire/fwdev.c:521
- sys/bus/firewire/fwdev.c:530
- sys/bus/firewire/fwdev.c:560
- sys/bus/firewire/fwdev.c:561
- sys/bus/firewire/fwdev.c:565
- sys/bus/firewire/fwdev.c:582
- sys/bus/firewire/fwdev.c:583
- sys/bus/firewire/fwdev.c:584
- sys/bus/firewire/firewire.c:210
- sys/bus/firewire/fwohci.c:521
- sys/bus/firewire/firewire.h:73
- sys/bus/firewire/firewire.h:265
- sys/kern/sys_generic.c:675
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.
No comments yet.