mesh_decap stack OOB write: reserved AE=11 mesh control grows the header copy to 56 bytes into the 50-byte stack buffer b[]
Summary
mesh_input never rejects the reserved AE=11 value of mc_flags&3, so meshdrlen = 6+3*6 = 24 and hdrspace = 56 for a DSTODS QoS frame; mesh_decap then does memcpy(b, mtod(m), hdrlen) into uint8_t b[50]. Six attacker-controlled bytes (the would-be mc_addr6) are written past b[] on the multicast data path, which has no AE validation on any kernel config. Remote unauthenticated-after-peering (victim auto-peers by default) kernel stack overwrite from a single 68-byte multicast mesh data frame. VERIFIED on stock #0: is_mesh_badae increments after the copy (proves the memcpy executed); shipped-binary disassembly proves the 50-byte object at rbp-0x62 receives a 56-byte rep movsb; on this gcc-8.3 INVARIANTS build the 6 bytes land in stack padding so it is silent, but any relayout (or 8 more bytes) reaches saved registers/return address - layout-latent control-flow corruption primitive, unlimited rate. uid0 hard blocker documented (2 bytes short of saved rbx on this build). Fix validated (AE>AE_10 rejected in mesh_input; AE=0 regression-tested).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3286 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fwm.c | β | 22.6 KB | view raw | |
| Makefile | β | 150 B | β download | |
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| run.log | β | 2.2 KB | view raw | |
| env.txt | β | 655 B | view raw | |
| build.sh | β | 87 B | view raw | |
| run.sh | β | 111 B | view raw | |
| fix.diff | β | 3.4 KB | view raw | |
| verdict.json | β | 3.2 KB | view raw | |
| manifest.json | β | 978 B | view raw |
DF-3286 β mesh_decap stack OOB write: reserved AE=11 mesh control grows header copy to 56 bytes into 50-byte stack buffer b[]
File: sys/netproto/802_11/wlan/ieee80211_mesh.c (pass 2 audit)
What
mesh_input (:1738-1741) computes the mesh-control length from the
attacker-supplied Address Extension bits without rejecting the reserved
value AE=11 (3):
ae = mc->mc_flags & IEEE80211_MESH_AE_MASK; /* 0..3, 3 = RESERVED */
meshdrlen = sizeof(struct ieee80211_meshcntl) /* 6 */
+ ae * IEEE80211_ADDR_LEN; /* 3*6 = 18 -> 24 */
hdrspace += meshdrlen; /* 32 + 24 = 56 */
mesh_decap (:1281-1298) then copies the full header into a stack buffer
sized for the legal maximum only:
uint8_t b[sizeof(struct ieee80211_qosframe_addr4) /* 32 */
+ sizeof(struct ieee80211_meshcntl_ae10)]; /* 18 -> 50 */
...
memcpy(b, mtod(m, caddr_t), hdrlen); /* hdrlen = 56 */
A 4-address (DSTODS) QoS mesh multicast data frame with
mc_flags & 3 == 3 from an established mesh peer writes 6
attacker-controlled bytes past b[] (the last 6 bytes of the 24-byte
mesh control, i.e. the would-be mc_addr6). The multicast path has no
AE validation anywhere (to_me/to_fwrd are unicast-only), so this
fires on INVARIANTS and production kernels.
Runtime proof on the stock kernel (X86_64_GENERIC #0, INVARIANTS)
-
Disassembly of the shipped kernel (mesh_decap inlined into mesh_input):
ffffffff80779e62: lea -0x62(%rbp),%r14 # b = rbp-0x62 (50 bytes: .. rbp-0x31) ffffffff80779e73: mov -0x98(%rbp),%rdx ffffffff80779e7a: rep movsb %ds:(%rsi),%es:(%rdi) # count = -0x90(%rbp) = hdrlen = 56The copy provably runs 56 bytes into the 50-byte object; the 6 extra bytes land atrbp-0x30..rbp-0x2b, which in this build is stack padding (saved registers start at rbp-0x28), so the guest survives β a silent, layout-latent kernel stack overwrite. -
Reachability: injecting the frame via the harness (
sysctl kern.fwm_step=10) on the stock guest leaves the guest up and incrementsis_mesh_badaeβ the frame provably reaches mesh_decap's post-memcpy AE dispatch (ieee80211_mesh.c:1339), i.e. the memcpy has already executed. Γ21 frames, still up (padding absorbs it here).
Build / run (repo root)
sh findings/poc/DF-3286/build.sh # builds /root/fwm/fwm.ko in the guest sh findings/poc/DF-3286/run.sh # brings mesh vap up, peers, fires step 10
Harness: fwm.c β software net80211 radio + RX frame injector + TX
capture (see README in this dir). The peering dance (beacon β OPEN β
CONFIRM, all synchronous) puts the victim in
IEEE80211_NODE_MESH_ESTABLISHED, after which a single 68-byte crafted
multicast data frame triggers the overflow.
Expected output
fwm: wlan0 RUN, beacon allocated fwm: inject BEACON ... / inject PEER OPEN / inject PEER CONFIRM fwm: inject MCAST AE=3 data (68 bytes; decap hdr=56 > b[50]) kern.fwm_stats: is_mesh_badae=1 ... # memcpy executed, frame then discarded GUEST_STILL_UP # 6 bytes hit padding on this build
Patched (fix.diff kernel): the AE=11 frame is discarded at mesh_input before any decap math; same run shows no decap-path activity for the frame and normal delivery for legal AE values.
Threat
Remote, unauthenticated-after-peering (victim auto-peers with any mesh
STA by default, ms_flags = AP|FWD at :683). 6 controlled bytes of
kernel-stack overwrite per frame; on this exact gcc-8.3 INVARIANTS build
they land in padding, on any rebuild/layout where b[] is adjacent to
live locals or spill slots this becomes stack corruption
(potential control-flow influence). CWE-787/CWE-121.
DF-3286 VERDICT β REPRODUCED (memory write proven on stock kernel; silent on this build's stack layout)
Bottom line
The 6-byte out-of-bounds stack write is proven to execute on the stock kernel, by three mutually-independent pieces of evidence:
-
Source math (certain):
meshdrlen = 6 + 3*6 = 24for AE=11,hdrspace = 32(DSTODS QoS), somesh_decap(..., hdrlen=56, ...)is handed 56 bytes for auint8_t b[50](sizeof(qosframe_addr4)=32+sizeof(meshcntl_ae10)=18) at sys/.../ieee80211_mesh.c:1281-1296, 1738-1741. -
Shipped-binary disassembly: in /boot/kernel/kernel (mesh_decap inlined at mesh_input+0xae2):
lea -0x62(%rbp),%r14(b base) β¦rep movsbwith count loaded from the hdrlen slot (-0x90(%rbp)) β the destination object is 50 bytes (rbp-0x62..rbp-0x31) and the runtime count is 56, so bytes rbp-0x30..rbp-0x2b are written out of object bounds. Those 6 bytes are the frame's would-bemc_addr6(fully attacker-chosen; 0xCC pattern in the PoC). -
Runtime reachability (stock guest, fwm harness): after
sysctl kern.fwm_step=10,is_mesh_badaeincrements β that counter is bumped at ieee80211_mesh.c:1339 after the memcpy, proving the copy executed on the live kernel. 21 consecutive frames: guest stays up.
Why no panic on this build
The overflow region rbp-0x30..rbp-0x2b is stack padding in this exact
gcc 8.3 -O2 INVARIANTS build (saved registers begin at rbp-0x28, 2 bytes
further up; no spill slot or local is addressed in the overflow window β
verified by scanning the full mesh_input disassembly). The bug is
nonetheless a genuine CWE-787 stack OOB write whose impact is
compiler-layout-dependent: 6 more bytes would reach saved rbx; any
relayout that places a live local or spill immediately above b[]
converts this into stack corruption of a pointer/int with
attacker-controlled data on a remote unauthenticated-after-peering path.
Exploit chain (honest ceiling)
none on this build (padding). Primitive: 6 controlled bytes at fixed offset above a 50-byte stack array in the RX path of every mesh data frame; rate-unlimited from any established peer.
Fix validation
Combined fix kernel (fix.diff hunk #4: reject ae > IEEE80211_MESH_AE_10
in mesh_input before meshdrlen math) built rc=0, rebooted, re-run:
- AE=11 frame now discarded at the new early gate (frame never reaches
mesh_decap; disassembly of the fixed kernel shows the new branch);
- legal AE=00 data frames still decap and deliver normally (no
regression);
- guest stays up across the full battery.
See fix section of run.log / fix notes in manifest.json.
Reproduce
see README.md; harness in this directory (fwm.c).
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
fixedAE=11 frames rejected at the new mesh_input gate (line-annotated disasm of fixed kernel mesh.c:1766-1770); the 56-byte decap memcpy is unreachable for AE=11; legal AE=0 data still decaps+delivers (no regression); no panic
['fix.log: binary proof + AE3_REJECTED_EARLY / AE0_DELIVERED_NO_CRASH']
Confirmed kernel references
Detail
Exploit chain
attacker mesh STA -> auto-peers with victim (default ms_flags AP|FWD) -> single 68-byte multicast DSTODS QoS data frame with mc_flags AE=11 -> mesh_input computes meshdrlen=24, pulls 56-byte header -> group path reaches mesh_decap -> memcpy overflows b[] by 6 attacker-chosen bytes -> (this build: padding; relayout: stack corruption)
Evidence (decisive lines)
run.log: is_mesh_badae=1 -> =21 after repeated step 10, GUEST_STILL_UP run.log: objdump excerpt - lea -0x62(%rbp),%r14 / rep movsb with hdrlen=56 VERDICT.md: full disasm + layout analysis
PoC changes
wrote mesh-capable harness from scratch (fwm.c, extends DF-3282 fw.c pattern): MBSS vap, synchronous beacon+OPEN+CONFIRM peering dance, RX injection via ieee80211_input_mimo, TX capture in ic_raw_xmit; learned victim mlpid from captured OPEN frames
Verified recommended fix
reject ae > IEEE80211_MESH_AE_10 in mesh_input before computing meshdrlen (see fix.diff hunk 4)
Verdict
mesh_decap memcpy(b, m_data, hdrlen) copies 56 bytes (DSTODS hdrspace 32 + AE=11 mesh control 24) into the 50-byte stack buffer b[sizeof(qosframe_addr4)+sizeof(meshcntl_ae10)] because mesh_input never rejects the reserved AE=11 bits. The 6-byte OOB write of attacker-controlled bytes (would-be mc_addr6) is proven to execute on the stock INVARIANTS kernel: shipped-binary disassembly shows the 50-byte object at rbp-0x62 and a rep movsb of 56, and the is_mesh_badae counter (bumped after the memcpy) increments on each injected frame. On this exact gcc-8.3 build the 6 bytes land in stack padding (saved registers begin 2 bytes beyond), so no crash is observed - the primitive is a layout-latent remote stack overwrite; 8 more bytes would reach saved rbx.
No comments yet.