Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ members = [
"lib/fat32/check",
"lib/gpt",
"lib/hda",
"lib/ieee80211",
"lib/input",
"lib/inspect",
"lib/logkeeper-api",
Expand Down
35 changes: 35 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,41 @@ a work of theirs. Two things the tools wrote are not:
of them are input a reader has to skip.


lib/ieee80211/tests/fixtures/tcpdump/ — 802.11 captures from tcpdump's tests
-----------------------------------------------------------------------------

ieee802.11_exthdr.pcap 4,499 bytes
sha256 5d1179c7045f3fe6a4a6621b758ee25c7a8ec1eece9d3d7be707969aa96a5236
ieee802.11_htc.pcap 466 bytes
sha256 a762951573320132a595a91833a1bc6c39c2bfd7f526c43f1402c307438fbb13
ieee802.11_meshhdr-oobr.pcap 126 bytes
sha256 cf4cff158b93f4e983abdf4d2fd97a6996bbfe36d82795b610fb128674794038
ieee802.11_meshid.pcap 823 bytes
sha256 9c64693b3f9d72365c198574ec0f4443c91c3d6dfa7f7a7d7ec420b14eb0cdbf
ieee802.11_parse_elements_oobr.pcap 295 bytes
sha256 5b89c6377bc6ada50619abce34ec0cbb3031790a79fffccaf44da610b9e91748
ieee802.11_rates_oobr.pcap 111 bytes
sha256 dff15ca82eb9814a34ea0d875ea5f63c0f1a5d178bf390ddc919bec7b3421c85
ieee802.11_rx-stbc.pcap 541 bytes
sha256 04322b0ee0cf314941e7e30c41378fbe96618a2b3952458dd04b9e8fbd581d75
ieee802.11_tim_ie_oobr.pcap 335 bytes
sha256 fbb8b20efb233ec789214ecf96d5365cc8a34c275dae2685061ac72486a20bda
Copyright: The Tcpdump Group and the captures' contributors
Licence text: licenses/BSD-3-Clause-tcpdump.txt (tcpdump's `LICENSE`,
sha256 8a54594d257e14a5260ac770f1633516cb51e3fc28c40136ce2697014eda7afd)
SPDX-License-Identifier: BSD-3-Clause

`toyos-ieee80211`'s host tests read these frames: bytes 802.11 radios and
other implementations wrote, from outside the Wi-Fi track's references. Each
is the file of that name under `tests/` in tcpdump's repository at commit
57768afc4cffbaa17faad7808f106bc325ada417
(https://github.com/the-tcpdump-group/tcpdump), unmodified. Four hold traffic: a
station scanning and joining, a mesh point's beacon and probes, and data frames,
protected and with HT Control. The four named `oobr` are hostile inputs tcpdump
recorded for its own over-reads. Nothing in this repository fetches
them, and no test reads tcpdump's dissection of them.


build/microcode/intel-ucode/* — Intel microcode, redistributable unmodified
---------------------------------------------------------------------------

Expand Down
49 changes: 49 additions & 0 deletions build/licence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,55 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[
"NOTICE",
Terms::Spdx("(MIT OR Apache-2.0) AND BSD-2-Clause"),
),
// Captures from tcpdump's test suite, read hermetically by `lib/ieee80211/tests/corpus.rs`.
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap",
"5d1179c7045f3fe6a4a6621b758ee25c7a8ec1eece9d3d7be707969aa96a5236",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_htc.pcap",
"a762951573320132a595a91833a1bc6c39c2bfd7f526c43f1402c307438fbb13",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshhdr-oobr.pcap",
"cf4cff158b93f4e983abdf4d2fd97a6996bbfe36d82795b610fb128674794038",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshid.pcap",
"9c64693b3f9d72365c198574ec0f4443c91c3d6dfa7f7a7d7ec420b14eb0cdbf",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap",
"5b89c6377bc6ada50619abce34ec0cbb3031790a79fffccaf44da610b9e91748",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rates_oobr.pcap",
"dff15ca82eb9814a34ea0d875ea5f63c0f1a5d178bf390ddc919bec7b3421c85",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap",
"04322b0ee0cf314941e7e30c41378fbe96618a2b3952458dd04b9e8fbd581d75",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap",
"fbb8b20efb233ec789214ecf96d5365cc8a34c275dae2685061ac72486a20bda",
"NOTICE",
Terms::Spdx("BSD-3-Clause"),
),
(
"assets/DOOM1.WAD",
"1d7d43be501e67d927e415e0b8f3e29c3bf33075e859721816f652a526cac771",
Expand Down
34 changes: 34 additions & 0 deletions issues/a-fragmented-wifi-frame-is-dropped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
status: open
kind: defect
opened: 2026-10-14
---

# A fragmented Wi-Fi frame is dropped

`toyos-ieee80211` refuses every fragment by name (`Refused::Fragmented`,
`lib/ieee80211/src/frame.rs`): a frame whose More Fragments bit is set or whose
Fragment Number is not 0. It does not reassemble the MSDU or MMPDU.

**The refusal is the right default under the trust model.** Frames are
untrusted input, and reassembly is where Vanhoef's fragment-and-forge attacks
live ("Fragment and Forge: Breaking Wi-Fi Through Frame Aggregation and
Fragmentation", USENIX Security 2021): fragments of different keys, of a frame
sent before a rekey, or a plaintext fragment mixed into an encrypted frame. A
reassembly buffer that does not exist cannot mix them.

**The cost is real.** Every MSDU that an access point or a peer fragments is
lost. 802.11 lets a transmitter fragment any unicast MSDU or MMPDU above its
fragmentation threshold (IEEE 802.11-2020). An AP configured with a
threshold below its MTU loses every MSDU above it silently: the radio
acknowledges each fragment before the parser sees it, so the sender never
retransmits, and every higher-layer retransmission of that MSDU is lost the
same way.

- **Owner:** the orchestrator, for the Wi-Fi track (`issues/there-is-no-wifi.md`).
- **Evidence:** the refusal and its test, `every_refusal_has_a_hostile_input_that_produces_it` in `lib/ieee80211/tests/refusals.rs`.
- **Exit:** reassembly that holds the fragment-and-forge defences, each refusal tested by host tests with the paper's attack frames:
- fragments reassembled only under one key and one packet-number run, with consecutive packet numbers;
- the buffer cleared on a rekey or reassociation;
- a plaintext fragment never joined to protected ones;
- a bounded buffer per sender and TID.
46 changes: 44 additions & 2 deletions issues/there-is-no-wifi.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ and owns each one this list gives no other owner.

- **Writers.** Every W stage's owner is the orchestrator, who briefs a
writer for it: an implementer told never to read the references or the
demo. The stage's design round is that writer's. The first W stage's pull
request, briefed to, adds that sentence to `.claude/agents/implementer.md`.
demo. The stage's design round is that writer's. Root `CLAUDE.md`'s
clean-room rule is that sentence; no prompt carries another.
- **Reviews.** The orchestrator's brief for each W stage's review and design
acceptance says that they quote no reference. A defect found through a
reference reaches the writer only as a reader's revision of the
Expand Down Expand Up @@ -230,6 +230,48 @@ Depends on: nothing; W6 waits on it.
`build/ssh/tests/fuzz.rs`; and a third-party capture corpus from outside the
references, beside the crate, with provenance, terms and `NOTICE` rows,
pinned by hash.
- **Owed: `lib/ieee80211`'s readings checked against the text.** W1 was
written without IEEE 802.11-2020, 802.11ax-2021 or the WFA WMM
Specification v1.2 at hand; each reading below rests on the writer's
knowledge, and on the captures where `lib/ieee80211/tests/corpus.rs` reads
them. Owner: the orchestrator.
- **Exit:** every reading below checked against the text; each one the text
contradicts fixed, and each leniency the text rules out turned into a
refusal by name.
- **Frame structure:** the protocol version, type 3 as Extension, the
reserved and contention-free subtypes; non-QoS Order as strictly
ordered; the four-address layout; Fragment Number and More Fragments;
`ManagementWithDs`; `GroupTransmitter` for a management SA and a data TA.
- **Control:** the BAR and BA Type values and TID_INFO; a BlockAckReq's
Fragment Number being 0; the HE bitmap sizes by Fragment Number bits 2
and 1, bit 0 a fragment level, bit 3 refused; the bandwidth-signaling TA.
- **Management bodies:** a beacon interval of 0 refused; the AID's two high
bits and its range of 1 to 2007.
- **Elements:** the Element ID Extension octet; each typed element allowed
once, WMM's included; the SSID's 32 octets; 1 to 8 Supported Rates and
Extended Supported Rates of length 0 refused; the selectors 127, 126, 123
and 122; the TIM's offset as N1/2, its bound of 251 octets and its minimum
of one bitmap octet; Country's string, triplets, operating triplet from
201 and channel bound of 200; MFPR as bit 6, MFPC as bit 7 and MFPR
without MFPC refused; the RSNXE's Field Length and SAE-H2E as bit 5; the
HT and VHT field splits and reserved values; Extended Capabilities' bit
numbering; HE Capabilities' 6, 11 and 4 octet fields, the 160 and 80+80
MCS sets under PHY Capabilities B3 and B4, PPE Thresholds Present as
B55, and the PPE length of 7 + 6 × (NSTS + 1) × RU count bits; HE
Operation's bits 14, 15 and 17 and its five-octet 6 GHz field; WMM's
layouts, version 1, AIFSN of at least 2 and each ACI once, and its exact
length.
- **Data and the window:** the A-MSDU subframe header and its padding; the
bridge-tunnel types 0x80F3 and 0x8137; CCMP's Ext IV bit; the §10.25.6
rules with the half-space boundary of 2048; the HE buffer of 256.
- **Each refusal's class**, `Malformed` or `Unsupported`.
- **The leniencies**, each to become a refusal if the text rules it out:
octets past the RSNE's and the RSNXE's last field ignored; octets past
the fields of HT and VHT Capabilities and Operation and of HE
Capabilities and Operation ignored (fewer refused); a Country element
accepted without its pad octet, and with one where none is needed. The
strict side, padding after an A-MSDU's last subframe refused, is checked
too.

**W2, the handshakes, in `wlan`**, sans-IO: the 4-way and group-key
handshakes, SAE by hash-to-element, PMF's keys, and the Transition Disable
Expand Down
7 changes: 7 additions & 0 deletions lib/ieee80211/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "toyos-ieee80211"
description = "IEEE 802.11 frames a station receives and sends, parsed in place and refused by name: management, block-ack control and data frames, their elements, A-MSDU, 802.11-to-Ethernet translation and the block-ack reorder window (pure, no_std with alloc, forbid(unsafe_code))."
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
publish = false
105 changes: 105 additions & 0 deletions lib/ieee80211/src/control.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! BlockAckReq (802.11-2020 §9.3.1.7) and BlockAck (§9.3.1.8) of the Compressed variant, the one an
//! HT, VHT or HE station's immediate block-ack agreement uses; their bodies follow Frame Control
//! and Duration.
//!
//! An HE BlockAck's bitmap is 32, 64, 128 or 256 bits long, as its Fragment Number subfield says
//! (802.11ax-2021 §9.3.1.8.2). Acknowledging fragments, the subfield's bit 0, is refused, because a
//! fragment is (`Refused::Fragmented`).

use crate::read::Reader;
use crate::{Address, Refused, Seq};

/// The BAR and BA Type value of the Compressed variant (Table 9-24, Table 9-28).
const COMPRESSED: u16 = 2;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockAckReq {
pub ra: Address,
pub ta: Address,
pub tid: u8,
/// The Starting Sequence Number: everything before it is no longer to be waited for.
pub ssn: Seq,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockAck {
pub ra: Address,
pub ta: Address,
pub tid: u8,
pub ssn: Seq,
/// Bit n acknowledges sequence number SSN + n, for n below `bits`.
pub bitmap: [u64; 4],
pub bits: u16,
}

impl BlockAck {
/// Whether `seq` is inside the bitmap and acknowledged by it.
pub fn acked(&self, seq: Seq) -> bool {
let offset = seq.since(self.ssn);
offset < self.bits
&& self
.bitmap
.get(usize::from(offset / 64))
.is_some_and(|word| word >> (offset % 64) & 1 != 0)
}
}

/// The receiver and transmitter, the control field's TID, and the Starting Sequence Control's
/// Fragment Number and sequence number.
fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, u8, u8, Seq), Refused> {
let ra = r.address()?;
// A VHT transmitter may set the TA's group bit as a bandwidth signaling TA (§9.3.1.1).
let Address([first, rest @ ..]) = r.address()?;
let [b1, b2, b3, b4, b5] = rest;
let ta = Address([first & !1, b1, b2, b3, b4, b5]);
let control = r.u16()?;
if (control >> 1) & 0x0F != COMPRESSED {
return Err(Refused::BlockAckVariant);
}
let [_, high] = control.to_le_bytes();
let [low, seq_high] = r.u16()?.to_le_bytes();
let seq = Seq::new(u16::from_le_bytes([low, seq_high]) >> 4);
Ok((ra, ta, high >> 4, low & 0x0F, seq))
}

impl BlockAckReq {
pub(crate) fn parse(after_duration: &[u8]) -> Result<Self, Refused> {
let mut r = Reader::new(after_duration);
let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?;
if fragment != 0 {
return Err(Refused::BitmapSize);
}
if !r.is_empty() {
return Err(Refused::ControlLength);
}
Ok(Self { ra, ta, tid, ssn })
}
}

impl BlockAck {
pub(crate) fn parse(after_duration: &[u8]) -> Result<Self, Refused> {
let mut r = Reader::new(after_duration);
let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?;
// Bits 2 and 1 of the Fragment Number give the bitmap's length, bit 0 a fragment level.
let bits: u16 = match fragment {
0b0000 => 64,
0b0010 => 256,
0b0100 => 128,
0b0110 => 32,
_ => return Err(Refused::BitmapSize),
};
let mut bitmap = [0u64; 4];
let mut octets = r.take(usize::from(bits / 8))?.iter();
for word in &mut bitmap {
let mut value = [0u8; 8];
for (slot, &octet) in value.iter_mut().zip(octets.by_ref()) {
*slot = octet;
}
*word = u64::from_le_bytes(value);
}
if !r.is_empty() {
return Err(Refused::ControlLength);
}
Ok(Self { ra, ta, tid, ssn, bitmap, bits })
}
}
Loading