Wrong TX frame-size bound causes 16-byte overflow of USB DMA buffer
Summary
rum_bulk_write_callback bounds outgoing mbuf length with if(m->m_pkthdr.len > (int)(MCLBYTES + RT2573_TX_DESC_SIZE)) and clamps to same value. Bound wrong: USB DMA buffer is MCLBYTES+RT2573_TX_DESC_SIZE+8=2080 bytes descriptor occupies first 24 bytes leaving only MCLBYTES+8=2056 bytes for frame. Check permits up to 2072 bytes so usbd_m_copy_in writes up to 16 bytes past end of page cache. usbd_copy_in/usbd_m_copy_in no internal bounds checking. Worse guard truncates m_pkthdr.len instead of dropping so 2346-byte 802.11 max-size frame becomes 2072-byte one still overflows by 16 bytes. Frames 2057..2072 bypass guard entirely. Sibling if_run.c:3091-3097 correct bound and DROP. Privileged user (CAP_NET_RAW/bpf monitor-mode vap) can submit raw 802.11 frames via rum_raw_xmit. [VERIFIED pass2: reproduced deterministically - canary bytes past the 2080-byte buffer equal frame pattern bytes 2056..2071 fully attacker-controlled 1..16 bytes, oversized frames truncate to 2072 then still overflow, submit_len 2096 exceeds bufsize; exploitation ceiling honestly capped - kmalloc 2080 lands in a 2304-byte chunk so overrun stays in same-chunk slack, no adjacent-object corruption no uid0; reach needs dongle plus CAP_NET_RAW; bound-and-drop fix-validated if_run-style, in-range frames bit-identical]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2357 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rumtest/rumtest.c | β | 12.4 KB | view raw | |
| rumtest/Makefile | β | 467 B | β download | |
| rumfix/rumfix.c | β | 12.4 KB | view raw | |
| rumfix/Makefile | β | 371 B | β download | |
| if_rum_fixed.c | β | 83.6 KB | view raw | |
| fix.diff | β | 694 B | view raw | |
| build.sh | β | 86 B | view raw | |
| run.sh | β | 110 B | view raw | |
| build.baseline.log | β | 6.3 KB | view raw | |
| build.fixed.log | β | 6.3 KB | view raw | |
| run.baseline.log | β | 10.2 KB | view raw | |
| run.fixed.log | β | 9.8 KB | view raw | |
| run.2.log | β | 10.2 KB | view raw | |
| run.3.log | β | 10.2 KB | view raw | |
| env.txt | β | 449 B | view raw | |
| VERDICT.md | β | 7.2 KB | β raw | |
| README.md | β | 3.0 KB | β raw | |
| verdict.json | β | 4.5 KB | view raw |
DF-2357 β Wrong TX frame-size bound causes 16-byte overflow of USB DMA buffer
sys/bus/u4b/wlan/if_rum.c β rum_bulk_write_callback (lines 1057-1065)
What this pack proves
Driving the verbatim, sha256-gated if_rum.c (compiled into a kernel
module with only the USB transfer-engine and node-completion externals
interposed) through the real raw-xmit chain
(rum_raw_xmit -> rum_tx_raw -> tx_q -> rum_bulk_write_callback
-> real usbd_copy_in / usbd_m_copy_in into an instrumented
2080-byte DMA buffer with canary):
| frame len | stock driver behavior | fixed driver behavior |
|---|---|---|
| 2048 | sent, submit_len 2072, canary clean | identical (bit-identical zone) |
| 2056 | sent, submit_len 2080 == bufsize, canary clean | identical (bit-identical zone) |
| 2057 | 1 byte past buffer (0x6f = frame byte 2056, attacker pattern) | dropped, canary clean |
| 2060 | 4 bytes past buffer | dropped |
| 2064 | 8 bytes past buffer | dropped |
| 2072 | 16 bytes past buffer, submit_len 2096 > 2080 | dropped |
| 2073 | truncated to 2072 -> 16 bytes past | dropped |
| 2346 | truncated to 2072 -> 16 bytes past | dropped |
| direct callback, 3000 | clamped to 2072 -> 16 bytes past | dropped |
The canary bytes are exactly the frame pattern bytes at offsets 2056..2071 β the overflow content is fully attacker-controlled.
How to reproduce (guest, as root)
cd /root/DF-2357 sh build.sh # sha256-gates if_rum.c, builds rumtest.ko + rumfix.ko sh run.sh # loads rumtest.ko then rumfix.ko, captures dmesg cat run.baseline.log # stock driver: canary hits cat run.fixed.log # fixed driver: drops, bit-identical in-range
Files
rumtest/rumtest.cβ the harness (interposition + fake USB xfer engine- canary-instrumented DMA buffer + test driver)
rumtest/Makefile,rumfix/Makefileβ kld module buildsrumfix/rumfix.cβ same harness compiled aroundif_rum_fixed.cif_rum_fixed.cβ verbatim if_rum.c +fix.diff(generated by build.sh)fix.diffβ the verified fix (git-apply-able at repo root)build.baseline.log,build.fixed.logβ full compiler outputrun.baseline.log,run.fixed.log,run.2.log,run.3.logβ full runtime output (runs 2 and 3 are stability repeats; 5x canary_hits=16 each, deterministic)env.txtβ guest uname / compiler / memory / kldstatVERDICT.mdβ full narrative incl. exploitability ceiling analysisverdict.json,manifest.json
Layout constants
MCLBYTES= 2048 (sys/sys/param.h:497, MCLSHIFT 11)RT2573_TX_DESC_SIZE= 24 (if_rumreg.h:22, packedstruct rum_tx_desc)- transfer bufsize = 2048+24+8 = 2080 (
if_rum.c:438) - frame capacity after descriptor = 2056; stock check allows 2072 -> 16-byte window (frames 2057..2072) plus truncate-then-overflow for any larger frame (e.g. max 802.11 frame 2346 -> clamped to 2072)
DF-2357 VERDICT β reproduced (primitive confirmed); exploitation ceiling capped by allocator slack on stock x86_64
Bottom line
The claimed bug is real and reproduces exactly as filed. The bound at
sys/bus/u4b/wlan/if_rum.c:1057 β m->m_pkthdr.len > (int)(MCLBYTES +
RT2573_TX_DESC_SIZE) (2072) β is wrong by 16 bytes for the transfer
buffer declared at if_rum.c:438 (bufsize = MCLBYTES +
RT2573_TX_DESC_SIZE + 8 = 2080, with the 24-byte TX descriptor written
first at offset 0), leaving only 2056 bytes for the frame. The guard also
TRUNCATES (if_rum.c:1060) instead of dropping, so an oversized frame
(anything > 2072, e.g. a maximum-size 2346-byte 802.11 frame) becomes a
2072-byte frame that still overflows. Sibling if_run.c:3091-3097 has
the correct shape and drops.
How it was proven
Verbatim-interposition kernel harness (rumtest/rumtest.c): the guest
kernel source if_rum.c is sha256-gated
(e11d7865440bbf95dce64336843a693e7251cd2747eb50ba0cfc6dbe8a992768,
identical to the audited tree) and compiled, unmodified, into a kld
module. Only four externals are interposed by textual #define:
usbd_transfer_start / usbd_transfer_submit (synchronous fake USB
callback engine that runs the driver callback under sc->sc_lock,
exactly like the real usb_callback_wrapper lock protocol established
by usbd_transfer_setup(..., &sc->sc_lock) at if_rum.c:488-489) and
ieee80211_tx_complete / ieee80211_free_node (node lifetime). The copy
path is the REAL stock kernel usbd_copy_in / usbd_m_copy_in
(sys/bus/u4b/usb_busdma.c:131-149, 203-209) operating on a real
struct usb_page_cache whose buffer is the instrumented 2080-byte
allocation followed by a 0xC5 canary zone. The usbd_get_page Plain-PIO
case (usb_busdma.c:119-126) computes res->buffer = pc->buffer +
offset with no bounds check β byte-for-byte the same write set the
real DMA-buffer case produces at these offsets.
Entry path driven: the REAL rum_raw_xmit (if_rum.c:3043, the
ic_raw_xmit handler reachable from a monitor-mode vap / bpf raw
frame) with crafted mbuf chains at lengths 2048, 2056, 2057, 2060,
2064, 2072, 2073, 2346, plus direct rum_bulk_write_callback drives at
2072 and 3000.
Observed (run.baseline.log, deterministic across 3 runs):
- len 2048: copy 24..2071, submit_len 2072 <= 2080, canary clean (control)
- len 2056: copy 24..2079 exactly fills the buffer, submit_len 2080 == bufsize, canary clean (true boundary)
- len 2057: 1 canary byte overwritten at offset 2080 with 0x6f β exactly pattern byte 2056 of the frame (attacker-controlled content)
- len 2060/2064: 4/8 bytes
- len 2072: 16 bytes, offsets 2080..2095, bytes 6f 7c 89 96 a3 b0 bd ca d7 e4 f1 fe 0b 18 25 32 == frame pattern bytes 2056..2071
- len 2073 and 2346: m_pkthdr.len observed at submit time = 2072 (the truncation), still 16 bytes past the buffer, submit_len 2096 > 2080
- direct callback len 3000: clamped to 2072, same 16-byte overflow (the clamp lives in the callback, not the feed path)
- additionally the submitted USB transfer length itself exceeds the buffer size (2084..2096 vs 2080), so the host controller DMA program covers 16 bytes beyond the allocation
Exploitability ceiling (honest assessment)
Reach requires an attached Ralink RT2501/RT2601/RT2573 USB dongle plus raw-xmit privilege (monitor-mode vap or CAP_NET_RAW / bpf write) β an already-privileged sender. That alone keeps this below High.
Even granting the privileged sender, the 16-byte overrun does NOT reach an adjacent kernel object on the stock x86_64 kernel:
- The TX buffer is allocated by
usb_pc_alloc_mem(usb_busdma.c:485-548) viabus_dmamem_alloc. - On this guest (x86_64, hw.physmem 4257873920 < 4 GB, 32-bit DMA tag
lowaddr 4 GB-1, alignment 1,
BUS_DMA_COHERENTis notBUS_DMA_NOCACHE) the kmalloc path applies (busdma_machdep.c:508-526,BUS_DMAMEM_KMALLOCat :88). kmalloc(2080, M_DEVBUF, ...)rounds up inzoneindex()(kern_slaballoc.c:668-671): the 2048..4095 range rounds to 256-byte granularity, so the chunk is 2304 bytes. Bytes 2080..2095 land inside the SAME chunk allocator slack. The page-boundary retry path (check_kmalloc->kmalloc(..., M_POWEROF2)-> 4096) and the contigmalloc fallback (dedicated 4096-byte pages) are even roomier.
So on every stock allocation path the write stays inside the object own padding: no adjacent-object overwrite, no length/pointer corruption, no uid=0 route. No panic or instability occurred across repeated runs. The only out-of-band effect on real hardware would be the over-long DMA transfer (submit_len up to 2096) reading 16 bytes of uninitialized heap slack and transmitting them over the air β a 16-byte heap-slack exposure per oversized frame to a radio sniffer; not verified on hardware (no dongle on the guest) and marginal.
The finding remains a legitimate memory-safety defect (CWE-787): it
violates the buffer contract the USB framework enforces nowhere else
(usbd_copy_in has no internal bounds check, usb_busdma.c:131-149),
and any future change in buffer sizing, tag alignment, or allocator
granularity (or a port with tighter packing) converts it into a real
cross-object heap overflow. Medium severity is the right call.
Fix validation (A/B on the stock kernel)
fix.diff replaces the wrong bound + truncate with the correct bound
(frame must fit bufsize - descriptor = MCLBYTES + 8 = 2056) and DROPS
the frame (rum_tx_free(data, 1); goto tr_setup; β the if_run.c shape).
Module-level A/B is the complete validation here: the stock kernel
binary contains no rum code (nm /boot/kernel/kernel shows zero rum_
symbols; the driver ships only as /boot/kernel/if_rum.ko), so building
the patched driver as a module against the same running kernel exercises
the exact patched code with zero kernel-image involvement. A full kernel
rebuild would add nothing for this driver.
Results (run.fixed.log vs run.baseline.log):
- len 2048 and 2056: sent, submit_len unchanged, full-zone hex digest lines byte-identical to the stock driver (BIT-IDENTICAL, verified by string comparison of the RUMTESTHEX lines)
- len 2057, 2060, 2064, 2072, 2073, 2346 and both direct drives: DROPPED β no submit, canary fully intact, tx_complete with error status (oerrors accounting via the error completion path), driver continues to the next queued frame (goto tr_setup)
Kernel references
- sys/bus/u4b/wlan/if_rum.c:438 (bufsize 2080)
- sys/bus/u4b/wlan/if_rum.c:1057-1065 (wrong bound + truncate + copy)
- sys/bus/u4b/wlan/if_rum.c:1080-1082 (xfer len roundup can hit 2096)
- sys/bus/u4b/usb_busdma.c:131-149 (usbd_copy_in, no bounds check)
- sys/bus/u4b/usb_busdma.c:203-209 (usbd_m_copy_in)
- sys/bus/u4b/usb_busdma.c:485-548 (buffer allocation path)
- sys/platform/pc64/x86_64/busdma_machdep.c:88,481-537 (kmalloc vs contigmalloc)
- sys/kern/kern_slaballoc.c:668-671 (zoneindex 2080 -> 2304 chunk)
- sys/bus/u4b/wlan/if_run.c:3091-3097 (correct sibling: bound + drop)
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_testableConfirmed kernel references
- sys/bus/u4b/wlan/if_rum.c:438
- sys/bus/u4b/wlan/if_rum.c:1057
- sys/bus/u4b/wlan/if_rum.c:1060
- sys/bus/u4b/wlan/if_rum.c:1064
- sys/bus/u4b/wlan/if_rum.c:1080
- sys/bus/u4b/usb_busdma.c:132
- sys/bus/u4b/usb_busdma.c:204
- sys/bus/u4b/usb_busdma.c:486
- sys/platform/pc64/x86_64/busdma_machdep.c:481
- sys/kern/kern_slaballoc.c:668
- sys/bus/u4b/wlan/if_run.c:3091
Detail
Evidence (decisive lines)
run.baseline.log lines len=2057/2060/2064/2072 (canary_hits 1/4/8/16, canary bytes 6f 7c 89 96 ... = frame pattern 2056..2071, submit_len EXCEEDS-BUFSIZE), len=2073 and 2346 (post_len=2072 proving truncation then 16-byte overflow), direct 3000 (clamped, 16 bytes); run.fixed.log (DROPPED for all overflow-window lengths, canary clean, tx_complete err=1) with RUMTESTHEX len=2048/2056 bit-identical to baseline; VERDICT.md carries the allocator analysis (kern_slaballoc.c:668-671, busdma_machdep.c:88,481-537)
PoC changes
Harness written from scratch (the finding had no seed PoC). Fixes vs the first draft: kmod.mk clobbers CFLAGS so -D/-I moved to COPTS; generated headers (opt_usb.h device_if.h bus_if.h usbdevs.h) added to SRCS; interposition defines moved before all declaring headers; usb_core.h/usb_busdma.h pre-included in usb_transfer.c order for full struct usb_xfer/usb_page_cache definitions; USB_ERR_OK replaced by the DragonFly name USB_ERR_NORMAL_COMPLETION; fake functions made non-static to match renamed prototypes; oerrors printf cast to int
Verified recommended fix
Replace the wrong bound and truncation in rum_bulk_write_callback with a correct bound of MCLBYTES + 8 (frame must fit bufsize minus the 24-byte descriptor) and DROP oversized frames via rum_tx_free(data, 1) + goto tr_setup, matching if_run.c
Verdict
Primitive reproduced deterministically on the stock 6.5-DEVELOPMENT guest with a verbatim sha256-gated if_rum.c compiled into a kld module and only the USB transfer engine and node completion interposed: rum_bulk_write_callback copies 24 desc bytes plus the frame from offset 24, the bound at if_rum.c:1057 admits frames up to 2072 bytes and truncates larger ones to 2072, so frames 2057..2072 (and any truncated frame, e.g. 2346 or 3000 reduced to 2072) overwrite exactly 1..16 fully attacker-controlled bytes past the 2080-byte transfer buffer (canary bytes equal frame pattern bytes 2056..2071), and the submitted transfer length itself reaches 2096 > 2080. Exploitation ceiling is honestly capped: reach needs the Ralink USB dongle plus CAP_NET_RAW or a monitor vap (already privileged), and on the stock x86_64 allocation path the 2080-byte request lands in a 2304-byte kmalloc chunk (zoneindex rounds 2080 to 2304; the M_POWEROF2 retry gives 4096; contigmalloc gives whole pages), so the 16 overrun bytes stay inside the same chunk slack and cannot corrupt an adjacent kernel object; no panic, no leak verified, no uid=0 route on this kernel. Real but latent CWE-787 contract violation worth fixing: Medium confirmed. Fix validated A/B at module level (the stock kernel image contains zero rum symbols, the driver is module-only, so a kernel rebuild adds nothing): patched driver drops all overflow-window and oversized frames with a clean canary and error completion, while in-range frames 2048 and 2056 produce byte-identical buffer contents to the stock driver.
No comments yet.