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

Uninitialized stack args[1] transmitted in mesh peer-timeout CLOSE action frames

Summary

mesh_peer_timeout_cb declares uint16_t args[3] and sets only args[0]/args[2] in the MAX_RETRIES and CONFIRM_TIMEOUT branches before sending a CLOSE action frame. args[1] (peer link id) is read by ieee80211_add_meshpeer: non-zero garbage selects the long-form IE and puts the two uninit kernel-stack bytes on air in the Peer Link ID field; it also decides the IE length. Any mesh peer triggers the timeouts (beacon then silence / confirm then silence) and receives the CLOSE frames. VERDICT: on the stock guest both timeout paths emitted short-form CLOSEs - the uninit slot read 0, so no bytes leaked at runtime on this build (not_reproduced/impact none; the read itself is source-certain). Fix validated (args[1]=0 at entry).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3289 Β· 4 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
run.sh β€” 111 B view raw
verdict.json β€” 2.3 KB view raw
manifest.json β€” 491 B view raw

DF-3289 β€” Uninitialized stack args[1] sent in mesh peer-timeout CLOSE action frames

File: sys/netproto/802_11/wlan/ieee80211_mesh.c (pass 2 audit)

What

mesh_peer_timeout_cb (:3091-3143) declares uint16_t args[3] and in two branches sets only elements 0 and 2 before transmitting a CLOSE action frame to the peer:

case IEEE80211_NODE_MESH_OPENSNT:
case IEEE80211_NODE_MESH_OPENRCV:
        if (ni->ni_mlrcnt == ieee80211_mesh_maxretries) {
                args[0] = ni->ni_mlpid;
                args[2] = IEEE80211_REASON_MESH_MAX_RETRIES;   /* args[1] NEVER set */
                ieee80211_send_action(ni, ..., MESHPEERING_CLOSE, args);
...
case IEEE80211_NODE_MESH_CONFIRMRCV:
        args[0] = ni->ni_mlpid;
        args[2] = IEEE80211_REASON_MESH_CONFIRM_TIMEOUT;       /* args[1] NEVER set */
        ieee80211_send_action(ni, ..., MESHPEERING_CLOSE, args);

args[1] flows into ieee80211_add_meshpeer(..., CLOSE, args[0], args[1], args[2]) (:2902-2948): if the garbage is non-zero the frame is built in long form (MPM_MAX_SZ) and the two uninitialized kernel-stack bytes are transmitted on air in the Peer Link ID field. The value also selects the IE length (6 vs 8) β€” an uninitialized-value use in protocol logic either way. CWE-457.

Any mesh peer can drive a victim into these branches: beacon β†’ victim auto-peers (OPENSNT) β†’ attacker stays silent β†’ 3Γ—40 ms β†’ CLOSE #1; CONFIRM-then-silence β†’ CONFIRMRCV timeout β†’ CLOSE #2.

Verification result (stock kernel)

Harness steps 1/3 reproduced the timeout CLOSE frames (captured at the radio): both the MAX_RETRIES and CONFIRM_TIMEOUT closes were emitted in short form (75 06 0000 <llid> <rcode>), i.e. on this build the uninitialized slot read as 0 and no bytes leaked on air. The uninitialized read itself is certain from source; the on-air disclosure depends on the stack garbage being non-zero, which was not observed on this kernel/callout path. Filed Low: latent info-leak/hygiene defect.

Captured CLOSE (CONFIRMRCV-timeout path):

tx[31] fc0=d0 len= 39: d0000000 02:00:00:00:00:aa > 02:00:00:00:00:f2 seq=0x2300
  0f 03 72 03 70776e 75 06 0000 eed1 3800      # close, meshid pwn, meshpeer len=6,
  #  llid=0xd1ee rcode=0x38(MAX_RETRIES) -- no peerid (uninit slot read 0)

Build / run

sh findings/poc/DF-3286/build.sh
sh findings/poc/DF-3289/run.sh     # beacon, confirm, silence, dump CLOSEs

Fix

args[1] = 0; at the top of mesh_peer_timeout_cb (fix.diff hunk 5).

Fix verification

fixed
baseline no→ patch + rebuild →patched clean

args[1]=0 initialized at cb entry (hunk 5); compiled into validated kernel; runtime observable unchanged on this build (slot read 0 already)

['fix.log: DF-3289 section']
DragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 10 05:34:08 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (fix kernel)

Confirmed kernel references

Detail

Exploit chain

attacker beacons victim into auto-peering then goes silent (or confirms then goes silent) -> victim timeout_cb builds CLOSE with uninit args[1] -> if garbage != 0, long-form meshpeer IE transmits 2 uninit kernel-stack bytes in the Peer Link ID field to the attacker

Evidence (decisive lines)

run.sh output: captured CLOSE 75 06 0000 <llid> 3800 (short form, slot read 0) on both timeout paths

PoC changes

passive observation via harness TX capture; no special trigger needed beyond beacon+silence

Verified recommended fix

args[1] = 0 at function entry (fix.diff hunk 5)

Verdict

mesh_peer_timeout_cb transmits CLOSE frames with args[1] (peer link id) never initialized in the MAX_RETRIES (:3108) and CONFIRM_TIMEOUT (:3126) branches - source-certain CWE-457. On the stock guest the captured timeout CLOSEs (both retry and confirm-timeout paths) were emitted in short form because the uninitialized stack slot read 0, so no kernel-stack bytes were observed on air and no behavior change was observable. Latent 2-byte kernel-stack info leak to any mesh peer whose stack garbage is non-zero.