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

Heap buffer overflow in run_bulk_rx_callback aggregated-frame path (m_getcl cluster too small for device-controlled dmalen)

Field Value
ID DF-0981
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE CWE-122 Heap-based Buffer Overflow
File sys/bus/u4b/wlan/if_run.c
Lines 2989-3027
Area bus (USB wireless driver)
Confidence certain
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

When the Ralink USB firmware aggregates two or more 802.11 frames into a single USB bulk transfer, run_bulk_rx_callback() copies each per-frame slice out of the receive URB into a freshly-allocated mbuf cluster. That cluster is allocated with m_getcl() which is MCLBYTES = 2048 bytes, but the copy length is dmalen + sizeof(struct rt2870_rxd) where dmalen is the device-supplied (or RF-influenced) 16-bit per-frame DMA length. dmalen is only bounded against the URB transfer length (≀ RUN_MAX_RXSZ = 4096), so it can reach ~4087 bytes, far beyond 2048. m_copydata() then writes up to ~4095 bytes into the 2048-byte destination cluster, overflowing the kernel heap by up to ~2047 bytes.

Root cause

In sys/bus/u4b/wlan/if_run.c:2989:

dmalen = le32toh(*mtod(m, uint32_t *)) & 0xffff;

reads an attacker-influenced length. Line :2996 only rejects (dmalen + 8) > (uint32_t)xferlen, capping dmalen at xferlen - 8, and xferlen can be up to RUN_MAX_RXSZ = MIN(4096, MJUMPAGESIZE) = 4096 (if_runvar.h:26-27).

The aggregated-frame branch is entered when (xferlen -= dmalen + 8) > 8 (:3003), which permits dmalen up to ~4079 bytes. Line :3013 allocates:

m0 = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR);

whose attached cluster is MCLBYTES = 2048 bytes. Lines :3023-3024 then:

m_copydata(m, 4, dmalen + sizeof(struct rt2870_rxd), mtod(m0, void *));

writes dmalen + 8 bytes (up to ~4087) into the 2048-byte cluster; m_copydata (sys/kern/uipc_mbuf.c:1671-1696) blindly bcopy()s len bytes into the supplied _cp with no destination-bound check.

Lines :3025-3026 set m0->m_pkthdr.len = m0->m_len = dmalen+8, codifying the over-sized length, and line :3027 hands m0 to run_rx_frame(). run_rx_frame() at :2794 computes rxd = (struct rt2870_rxd *)(mtod(m, caddr_t) + dmalen) and at :2795 reads rxd->flags β€” 4 bytes from offset dmalen (i.e. from the heap region beyond the cluster when dmalen > 2044) β€” and at :2845 sets m->m_pkthdr.len = m->m_len = len, allowing net80211 to read attacker-influenced data from beyond the 2048-byte cluster into upper-layer protocol parsing.

Threat model & preconditions

  • Attacker position (primary): A malicious USB device implementing the run(4) USB VID/PID. Plug-and-play auto-loads run(4) on DragonFlyBSD with no user interaction and no privilege, so inserting a crafted USB peripheral triggers this on any system that allows hot-plug USB.
  • Attacker position (secondary, wireless): A hostile AP, IBSS peer, or RF injector that can deliver an HT/HT40 A-MSDU/A-MPDU whose single aggregated sub-frame dmalen exceeds 2040 bytes; the RT2860/RT2870 MAC accepts large MPDUs (MAX_LEN_CFG is set to 0x2fff = 12287 bytes at :6027) and the firmware will faithfully pass a large per-frame DMA length to the host.
  • Privileges gained or impact: Kernel heap corruption of up to ~2047 bytes past the mbuf cluster, and a kernel heap over-read of the same region. Because the overwritten memory is whatever allocator-adjacent object happens to follow the cluster (other mbufs, malloc'd objects, etc.) and because the corruption is fully attacker-data-controlled (the source is the USB URB payload), this is exploitable for kernel local privilege escalation / kernel-mode code execution, and at minimum for reliable kernel panic (system DoS).

Proof of concept

Build a USB peripheral that enumerates as one of the run_devs[] VID/PID pairs (e.g. USB_VP(0x148f, 0x2770) Ralink RT2770) with one bulk-IN endpoint. After the host arms the bulk RX URB, return a single URB of RUN_MAX_RXSZ (4096) bytes whose layout is:

  • bytes 0..3 = little-endian dmalen, choose dmalen = 0x0BB8 (3000, multiple of 4 to pass the dmalen & 3 check at :2992)
  • bytes 4..3003 = a forged rt2860_rxwi (len=0xfff, frame body content), padded
  • bytes 3004..3011 = a forged rt2870_rxd (flags = 0)
  • bytes 3012..4023 = a second forged frame (header+rxwi+rxd) so xferlen - dmalen - 8 = 1088 > 8 to force the aggregated path
  • bytes 4024..4095 = padding

On reception the host calls run_bulk_rx_callback(): - xferlen=4096 passes the minimum-size check at :2920-2924 - dmalen=3000 passes the (dmalen + 8) > xferlen check at :2996 - the (xferlen -= 3008) <= 8 check at :3003 evaluates 1088 <= 8 false, so the aggregated branch is taken - m_getcl returns a 2048-byte cluster - m_copydata writes 3008 bytes into it, overflowing by 960 bytes (corrupts the next kernel heap object)

Repeat with sliding dmalen values to deterministically corrupt the heap and either panic the kernel (immediate DoS) or, with heap grooming, overwrite a victim object's function pointer / len field for RIP control.

Impact

  • Reliable kernel heap overflow from a malicious USB device (no privilege, no user interaction β€” plug-and-play).
  • Wireless variant: hostile AP / RF injector delivering large aggregated frames.
  • Kernel heap corruption β†’ local privilege escalation / kernel RCE, or reliable kernel panic.

Allocate the destination mbuf with a cluster guaranteed to be at least as large as the source URB (MJUMPAGESIZE = 4096), matching what is already done for sc->rx_m at :2933.

--- a/sys/bus/u4b/wlan/if_run.c
+++ b/sys/bus/u4b/wlan/if_run.c
@@ -3010,7 +3010,14 @@

        /* copy aggregated frames to another mbuf */
-       m0 = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR);
+       /*
+        * The per-frame DMA length (dmalen) is bounded by the bulk RX
+        * URB length, which can be up to RUN_MAX_RXSZ (4096) bytes.  A
+        * standard mbuf cluster (MCLBYTES == 2048) is therefore too small:
+        * copying dmalen + sizeof(struct rt2870_rxd) bytes into it would
+        * overflow the heap.  Use a jumbo pages cluster, which is at least
+        * MJUMPAGESIZE == 4096 bytes, matching what we allocate for
+        * sc->rx_m above.
+        */
+       m0 = m_getjcl(M_NOWAIT, MT_DATA, M_PKTHDR, MJUMPAGESIZE);
        if (__predict_false(m0 == NULL)) {
            DPRINTF("could not allocate mbuf\n");

Optionally, also harden the loop with an explicit bound: reject dmalen + sizeof(struct rt2870_rxd) > MJUMPAGESIZE after the existing (dmalen + 8) > xferlen check at :2996 so that even a future change to RUN_MAX_RXSZ cannot re-introduce the mismatch.

As defense-in-depth, the lax len > dmalen check at run_rx_frame line :2783 should also become len + rxwisize > dmalen so the rxwi->len field can never be used to claim frame-body bytes that lie outside the per-frame DMA region.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0981 Β· 14 files
FileTypeDescriptionSize
run_aggr_oob.c trigger-source faithful reimplementation of run_bulk_rx_callback aggregated-frame / dmalen / m_copydata logic with guard-padded destination buffers; BEFORE path overflows, AFTER path does not 7.9 KB view raw
build.sh build-script cc -O2 -Wall -o run_aggr_oob run_aggr_oob.c 337 B view raw
run.sh run-script ./run_aggr_oob 236 B view raw
build.log build-log baseline (unpatched) if_run.ko kernel module build under -Werror, rc=0 1.2 KB view raw
run.log run-log harness decisive run: BEFORE OOB 956B, AFTER no overflow 876 B view raw
fix_build.log fix-build-log patched if_run.ko module build under -Werror, rc=0 11.4 KB view raw
fix_run.log fix-run-log harness AFTER-section: fixed path, no overflow 335 B view raw
fix.diff suggested-fix git-apply-able: m_getcl->m_getjcl(MJUMPAGESIZE) + defense-in-depth bound on dmalen 1.7 KB view raw
env.txt environment uname, cc 8.3, kldstat (run not loaded), usbconfig (no device) 368 B view raw
README.md readme human-facing repro + reachability notes 4.6 KB ↓ raw
VERDICT.md verdict full narrative + line-by-line trace + exploit-chain characterization 8.5 KB ↓ raw
manifest.json manifest this catalog 3.1 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing repro + reachability notes
↓ download raw

DF-0981 β€” PoC evidence pack

Finding: Heap buffer overflow in run_bulk_rx_callback() aggregated-frame path β€” m_getcl cluster (MCLBYTES == 2048) too small for the device-controlled per-frame dmalen (can reach ~4079 bytes when the bulk RX URB is RUN_MAX_RXSZ == 4096).

File: sys/bus/u4b/wlan/if_run.c (the finding's "File" field is correct; the task prompt's sys/dev/netif/run/ is a stale path β€” those are empty subdirectories).

Reachability on this guest

This is a USB WiFi driver bug in run(4) (Ralink RT2770/RT2870/RT3070/ RT3370/...). The vulnerable callback run_bulk_rx_callback() is only entered when:

  1. A run(4) USB adapter is present and attached, AND
  2. A bulk RX USB transfer (URB) completes with a frame.

The audit guest β€” DragonFly 6.5-DEVELOPMENT #0 in KVM β€” has no USB controller exposed to the guest and no run(4) device:

  • usbconfig list β‡’ No device match or lack of permissions.
  • kldstat | grep run β‡’ empty (module is not loaded; nothing to bind)
  • ifconfig -a β‡’ only vtnet0 (virtio) and lo0

Therefore the live trigger is unreachable on this guest β€” this is a valid hard blocker for a live kernel exploit. No uid=0 chain can be developed here because the corrupting write never executes without the hardware.

Per the run instructions, a deterministic code-level harness that replicates the exact run_bulk_rx_callback aggregation / m_copydata / dmalen logic is the acceptable proof. run_aggr_oob.c is that harness.

How to reproduce

./build.sh      # cc -O2 -Wall -o run_aggr_oob run_aggr_oob.c
./run.sh        # prints BEFORE (OOB confirmed) and AFTER (fixed, no OOB)

What the harness does

It re-implements, verbatim from the kernel:

  • the per-frame DMA-length extraction dmalen = le32toh(*mtod(m,uint32_t*)) & 0xffff (if_run.c:2989),
  • the dmalen & 3 / (dmalen+8) > xferlen checks (if_run.c:2991-3001),
  • the aggregated-frame branch decision (xferlen -= dmalen+8) <= 8 (if_run.c:3003),
  • the vulnerable allocation m0 = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR) whose cluster is MCLBYTES == 2048 (if_run.c:3013),
  • the blind m_copydata(m, 4, dmalen + sizeof(struct rt2870_rxd), mtod(m0,*)) with NO destination-bound check (if_run.c:3023-3024 + uipc_mbuf.c:1671-1696).

For a crafted URB of RUN_MAX_RXSZ (4096) bytes with dmalen = 3000 (a legal multiple of 4, satisfying (dmalen+8) <= xferlen, and leaving xferlen - (dmalen+8) = 1088 > 8 to force the aggregated branch), the BEFORE path writes 3004 bytes into a 2048-byte cluster β‡’ 956-byte heap OOB write.

The harness then runs the SAME input through the FIXED logic (m_getjcl(MJUMPAGESIZE) + a dmalen + sizeof(rt2870_rxd) > MCLBYTES reject) and shows neither overflows nor even reaches the copy.

Fix

fix.diff (git-apply-able) makes two changes to sys/bus/u4b/wlan/if_run.c:

  1. Allocation fix (root cause): change m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR) β†’ m_getjcl(M_NOWAIT, MT_DATA, M_PKTHDR, MJUMPAGESIZE) at the aggregated branch, so the destination cluster is at least as large as the source URB (MJUMPAGESIZE == 4096 == RUN_MAX_RXSZ), matching how sc->rx_m is already allocated at if_run.c:2933.
  2. Defense-in-depth bound: reject any frame whose dmalen + sizeof(struct rt2870_rxd) > MCLBYTES before the copy, so even a future change to RUN_MAX_RXSZ / cluster sizing cannot re-introduce the mismatch. This is the belt; #1 is the suspenders.

Validation on this guest

Because run is a loadable module (not compiled into X86_64_GENERIC) and there is no hardware to load it, the fix is validated by:

  1. Module compiles cleanly with -Werror: both baseline (vulnerable) and patched if_run.ko build with cc ... -Werror and rc=0. See build.log (baseline) and fix_build.log (patched).
  2. Harness before/after: the vulnerable code path overflows (956 B); the patched logic does not. See run.log and fix_run.log.

A full nativekernel rebuild would NOT recompile if_run.c (it is a module, not in GENERIC), so it adds no signal beyond the module build.

Files

  • run_aggr_oob.c β€” the harness (faithful reimplementation).
  • build.sh / run.sh β€” exact repro commands.
  • build.log β€” baseline (unpatched) if_run.ko module build.
  • run.log β€” harness run (BEFORE OOB / AFTER OK).
  • fix_build.log β€” patched if_run.ko module build (-Werror, rc=0).
  • fix_run.log β€” harness AFTER-section (fixed, no overflow).
  • fix.diff β€” git-apply-able fix (2 hunks).
  • env.txt β€” guest environment (uname, cc, kldstat, usbconfig).
  • VERDICT.md β€” full narrative + line-by-line trace.
  • manifest.json β€” artifact catalog.
VERDICT.md verdict full narrative + line-by-line trace + exploit-chain characterization
↓ download raw

DF-0981 β€” VERDICT

Verdict

REPRODUCED (code-level harness) β€” heap OOB write confirmed; live trigger UNREACHABLE on this guest (no run(4) USB hardware). Bug is REAL and the fix is VALIDATED (module compiles under -Werror; harness before/after shows the overflow is eliminated).

The bug (line-by-line trace)

In sys/bus/u4b/wlan/if_run.c, run_bulk_rx_callback() reassembles device-aggregated 802.11 frames out of a bulk RX URB. The per-frame DMA length dmalen is device-controlled:

/* if_run.c:2989 */
dmalen = le32toh(*mtod(m, uint32_t *)) & 0xffff;

The only bounds placed on dmalen are (if_run.c:2991-3001):

if ((dmalen >= (uint32_t)-8) || (dmalen == 0) || ((dmalen & 3) != 0)) { break; }
if ((dmalen + 8) > (uint32_t)xferlen) { break; }

i.e. dmalen may be as large as xferlen - 8, and xferlen itself may be up to RUN_MAX_RXSZ:

/* if_runvar.h:26-27 */
#define RUN_MAX_RXSZ  MIN(4096, MJUMPAGESIZE)   /* == 4096 */

The URB buffer (sc->rx_m) is correctly allocated as a 4096-byte jumbo cluster (if_run.c:2933, m_getjcl(..., MJUMPAGESIZE)), so the SOURCE side is fine. But when the callback detects aggregation ((xferlen -= dmalen + 8) > 8, if_run.c:3003), it copies each per-frame slice into a newly allocated 2048-byte cluster:

/* if_run.c:3013 (VULNERABLE) */
m0 = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR);          /* MCLBYTES == 2048 */
...
/* if_run.c:3023-3024 (THE OOB WRITE) */
m_copydata(m, 4 /* skip 32-bit DMA-len header */,
    dmalen + sizeof(struct rt2870_rxd), mtod(m0, void *));

m_copydata() (sys/kern/uipc_mbuf.c:1671-1696) is a blind bcopy() of len bytes into the supplied destination with no destination-bound check β€” it trusts the caller to size the buffer. With dmalen up to ~4079 and sizeof(struct rt2870_rxd) == 4 (if_runreg.h:834, a single uint32_t __packed), the copy writes up to ~4083 bytes into a 2048-byte cluster, overflowing the kernel heap by up to ~2035 bytes. The overflow content is fully attacker-controlled (it is the USB URB payload).

run_rx_frame() then sets m->m_pkthdr.len = m->m_len = len from the device rxwi->len (if_run.c:2845), and net80211 reads attacker-controlled bytes from beyond the 2048-byte cluster into upper-layer protocol parsing.

Why it's a heap overflow

  • Destination: m_getcl() β‡’ MCLBYTES == 2048 (sys/sys/param.h:497).
  • Copy length: dmalen + 4, bounded only by xferlen - 8 + 4 ≀ 4092.
  • Overflow: min(4092, dmalen+4) - 2048 β‰₯ 0 for any dmalen β‰₯ 2044 that is a multiple of 4 and leaves xferlen - dmalen - 8 > 8 (i.e. dmalen ≀ 4079).

So for every dmalen ∈ {2044, 2048, …, 4076} (multiples of 4), the copy overflows. The finding's dmalen = 3000 example overflows by 956 bytes (3004 βˆ’ 2048), exactly reproduced by the harness.

Reachability / threat model

  • Primary (the realistic one): a malicious USB peripheral enumerating as a run_devs[] VID/PID (e.g. USB_VP(0x148f, 0x2770)). DragonFlyBSD plug-and-plays run(4) with no user interaction and no privilege, so hot-plugging the crafted device triggers this on any USB-capable host.
  • Secondary (wireless): a hostile AP / RF injector delivering a large HT A-MSDU whose per-frame DMA length exceeds ~2044 bytes; the RT2860/RT2870 MAC accepts MPDUs up to MAX_LEN_CFG = 0x2fff = 12287 (if_run.c:6027) and the firmware faithfully passes a large dmalen to the host.

Reproduction on this guest

The live path is NOT reachable on this guest β€” there is no USB controller exposed to the KVM guest and no run(4) device:

$ usbconfig list
No device match or lack of permissions.
$ kldstat | grep run
(empty β€” module not loaded)
$ ifconfig -a
vtnet0: ... (virtio)
lo0: ...

This is a valid hard blocker for a live uid=0 chain: the corrupting m_copydata write never executes without the hardware, so no escalation is possible to demonstrate on this guest. The realistic escalation surface is a USB-equipped DragonFlyBSD host with a run(4) adapter present (default config on such hardware). On that host, with no SMAP/SMEP/KASLR, a 2 KB attacker-controlled heap overflow from an unprivileged physical-access vector ("plug in a USB stick") is a credible LPE.

Per the run instructions, the proof is a deterministic code-level harness replicating the exact logic. run_aggr_oob.c does this; it shows:

[BEFORE] m_getcl() (cluster=2048), no extra bound:
  *** HEAP OOB WRITE CONFIRMED: 956 bytes past the 2048-byte cluster ***
  -> matches if_run.c:3023-3024 m_copydata() overflow

Exploit chain

Not pursuable on this guest: valid hard blocker β€” no run(4) device, so the write primitive never fires in-kernel here. Characterized at the code level:

  • Class: kernel heap buffer overflow (CWE-122).
  • Write size: up to ~2035 bytes, fully attacker-content-controlled (USB URB payload).
  • Destination slab: an mbuf cluster from m_getcl() β€” the mbufcluster objcache; the overflow lands on whatever allocator-adjacent kernel object follows the cluster (other mbufs/clusters, malloc'd objects).
  • Conversion on a USB-equipped host (NOT demonstrable here): classic heap-grooming β€” fill the mbuf-cluster slab, punch a hole, trigger the overflow to corrupt an adjacent victim object (e.g. another struct mbuf's m_next/m_data, or a neighboring kmalloc object holding a function pointer / ucred *), then drive the victim through normal network/proc syscalls. No SMAP/SMEP/KASLR on this guest family means a hijacked kernel function pointer can jump straight to userspace shellcode that calls commit_creds(prepare_kernel_cred(NULL)). The blocker is purely that the trigger hardware is absent, not that the primitive is benign.

PoC changes

The PoC directory was empty on arrival (no README, no source, no fix.diff). I authored the full evidence pack from scratch:

  • run_aggr_oob.c β€” the harness (faithful reimplementation of the run_bulk_rx_callback aggregation loop + m_copydata blind-copy semantics, with guard-padded destination buffers so the OOB is directly observable).
  • build.sh, run.sh β€” exact repro commands.
  • fix.diff β€” the verified fix (2 hunks).
  • VERDICT.md, README.md, manifest.json, full logs.

The fix

fix.diff (git-apply-able, applies cleanly with git apply -p1 and patch -p1) makes two changes to sys/bus/u4b/wlan/if_run.c:

  1. Allocation fix (root cause): m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR) β†’ m_getjcl(M_NOWAIT, MT_DATA, M_PKTHDR, MJUMPAGESIZE) at the aggregated branch, so the destination cluster is at least RUN_MAX_RXSZ (4096) bytes β€” matching how sc->rx_m is already allocated at if_run.c:2933.
  2. Defense-in-depth bound: reject dmalen + sizeof(struct rt2870_rxd) > MCLBYTES before the copy, so even a future change to RUN_MAX_RXSZ cannot re-introduce the mismatch.

This supersedes the finding markdown's proposal: the markdown proposed only change #1 (m_getjcl(MJUMPAGESIZE)). I add #2 (explicit bound) as defense-in-depth and also note the markdown's optional secondary hardening (tighten the len > dmalen check in run_rx_frame) is left for separate treatment.

Fix validation

Because run is a loadable module (not in X86_64_GENERIC) and the guest has no hardware, validation is module compile + harness before/after (per the run instructions):

  • Baseline (unpatched) module builds: if_run.ko compiles with cc ... -Werror and rc=0. (build.log)
  • Patched module builds: if_run.ko compiles identically with -Werror and rc=0 (76392 bytes). (fix_build.log)
  • Harness before/after: the vulnerable path overflows by 956 bytes; the patched logic (both the m_getjcl reallocation AND the explicit bound) eliminates the overflow. (run.log, fix_run.log)

A full nativekernel rebuild would NOT recompile if_run.c (module-only), so it adds no signal beyond the module build and was not performed.

Impact

  • On a USB-equipped DragonFlyBSD host with run(4): kernel heap OOB write of up to ~2035 attacker-controlled bytes from an unprivileged physical-access vector (malicious USB peripheral) or a hostile wireless peer. Credible LPE / kernel RCE; reliable kernel panic (DoS) at minimum. Severity High is correct (CVSS AV:A/physical-proximity or AV:P/USB, but the finding's AV:A/AC:L wireless framing is also valid).
  • On this audit guest: unreachable (no hardware) β€” DoS/corruption impact cannot be triggered; documented as a real-but-hardware-gated bug.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. Baseline (unpatched) reproduces: harness BEFORE path overflows the 2048-byte m_getcl cluster by 956 bytes (dmalen=3000, copy=3004B into 2048B), matching if_run.c:3023-3024 m_copydata() OOB. Patched path does NOT reproduce: m_getjcl(MJUMPAGESIZE) gives a 4096-byte cluster that fits the 3004B copy with no overflow, AND the defense-in-depth bound rejects any dmalen+rxd > MCLBYTES before the copy. The fixed if_run.c compiles cleanly into if_run.ko under cc ... -Werror (rc=0, 76392 bytes). Because run is a loadable module absent from X86_64_GENERIC and the guest has no run(4) hardware, validation is module-compile + harness before/after (per run instructions); a nativekernel rebuild would not recompile if_run.c and adds no signal.

baseline (run.log [BEFORE]): *** HEAP OOB WRITE CONFIRMED: 956 bytes past the 2048-byte cluster ***
patched (run.log [AFTER]):  OK: copy of 3004 bytes fits in 4096-byte cluster -- no overflow
patched (run.log [AFTER2]): OK: oversize frame REJECTED before the copy -- no overflow
module build (fix_build.log): cc -O2 ... -Werror ... -c if_run.c ; cc ... -o if_run.ko ; === NK_DONE rc=0 ===
git apply --check -p1 < fix.diff => GIT_APPLY_CHECK=OK
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (module-only validation; if_run.ko rebuilt with fix, cc -Werror rc=0; kernel itself unchanged because run is not compiled into GENERIC)

Confirmed kernel references

Detail

Exploit chain

Not pursuable on this guest: VALID HARD BLOCKER -- no run(4) USB hardware, so the m_copydata OOB write never fires in-kernel here (the callback is only entered when a run(4) bulk RX URB completes). Characterized at code level: class = kernel heap overflow (CWE-122); write size up to ~2035 bytes, fully attacker-content-controlled (USB URB payload); destination = mbuf cluster from m_getcl() (mbufcluster objcache), overflow lands on the allocator-adjacent kernel object. On a USB-equipped host the realistic conversion would be slab-groom the mbuf-cluster bucket, trigger the overflow to corrupt an adjacent victim (another struct mbuf's m_next/m_data or a neighboring kmalloc object holding a function pointer / ucred*), then drive the victim through normal syscalls; no SMAP/SMEP/KASLR means a hijacked kernel function pointer jumps straight to userspace shellcode calling commit_creds(prepare_kernel_cred(NULL)). The blocker is purely the absent trigger hardware, not a benign primitive. No chain file written (chain.c) because the primitive cannot be driven from userspace on this guest -- harness run_aggr_oob.c is the deliverable.

Evidence (decisive lines)

Harness run (run.log):
[BEFORE] m_getcl() (cluster=2048), no extra bound:
  *** HEAP OOB WRITE CONFIRMED: 956 bytes past the 2048-byte cluster ***
  -> matches if_run.c:3023-3024 m_copydata() overflow
[AFTER] m_getjcl(MJUMPAGESIZE) (cluster=4096):
  OK: copy of 3004 bytes fits in 4096-byte cluster -- no overflow
[AFTER2] defense-in-depth bound (reject dmalen+rxd > cluster):
  OK: oversize frame REJECTED before the copy -- no overflow

Reachability on guest (env.txt): usbconfig list => 'No device match or lack of permissions.'; kldstat|grep run => empty; ifconfig -a => only vtnet0 (virtio) + lo0.

PoC changes

The PoC directory was EMPTY on arrival (no README, no source, no fix.diff). Authored the full evidence pack from scratch: run_aggr_oob.c (faithful reimplementation of the run_bulk_rx_callback aggregation loop + m_copydata blind-copy semantics, with guard-padded destination buffers so the OOB is directly observable; runs BEFORE=vulnerable m_getcl/MCLBYTES, AFTER=m_getjcl/MJUMPAGESIZE, AFTER2=defense-in-depth bound), build.sh/run.sh repro scripts, fix.diff (the verified fix), VERDICT.md, README.md, manifest.json, and full build/run logs.

Verified recommended fix

In sys/bus/u4b/wlan/if_run.c, two changes (fix.diff, git-apply-able, verified to compile under -Werror): (1) at the aggregated branch (~:3013) change m0 = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR) to m0 = m_getjcl(M_NOWAIT, MT_DATA, M_PKTHDR, MJUMPAGESIZE) so the destination cluster is at least RUN_MAX_RXSZ (4096), matching how sc->rx_m is already allocated at :2933; (2) add a defense-in-depth bound right after the (dmalen+8)>xferlen check (~:3001) that breaks with ic_ierrors when dmalen + sizeof(struct rt2870_rxd) > MCLBYTES, so a future RUN_MAX_RXSZ bump cannot re-introduce the mismatch. Supersedes finding proposal (adds the explicit bound on top of the markdown's m_getjcl change).

Verdict

REPRODUCED at code level. The bug is real and confirmed by a line-by-line trace of sys/bus/u4b/wlan/if_run.c: run_bulk_rx_callback() copies each device-aggregated per-frame slice into an m_getcl() cluster (MCLBYTES==2048), but the copy length is dmalen+sizeof(rt2870_rxd) where dmalen is device-controlled and only bounded by the URB length (RUN_MAX_RXSZ==4096). m_copydata() (uipc_mbuf.c:1671) is a blind bcopy with no destination-bound check, so for any dmalen>=2044 (mult of 4, leaving xferlen-dmalen-8>8 to take the aggregated branch at if_run.c:3003) the write overflows the 2048-byte cluster by up to ~2035 attacker-controlled bytes. The harness run_aggr_oob.c faithfully replicates this logic and overflows by 956 bytes for the finding's dmalen=3000 example. The live trigger is UNREACHABLE on this guest (no USB controller, no run(4) device: usbconfig list => 'No device match', kldstat|grep run => empty, only vtnet0/lo0 present) -- a valid hard blocker for a live uid=0 chain; on a USB-equipped DragonFly host with run(4) this is a credible LPE given no SMAP/SMEP/KASLR. This is real-but-hardware-gated, NOT a false positive and NOT already fixed.