From d3be04c1237df343b0793dc040d1b6a011163574 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 03:29:56 +0200 Subject: [PATCH 1/4] toyos-ieee80211: 802.11 frames a station meets, parsed in place and refused by name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W1 of the Wi-Fi track (issues/there-is-no-wifi.md), written clean-room from IEEE Std 802.11-2020, RFC 1042, IEEE 802.1H and the reader's specification §11 (evidence pr-876 at bc450203d6), reading no other implementation's source. - frame: the MAC header of every management subtype a station takes part in, BlockAckReq and BlockAck of the Compressed variant, and Data, Null, QoS Data and QoS Null, with HT Control and the four-address format. A fragment is refused rather than reassembled. - mgmt: each management body's fixed fields. - element: the TLV walk, and SSID, Supported and Extended Supported Rates, DSSS Parameter Set, TIM, Country, RSN, RSNXE, HT and VHT Capabilities and Operation and Extended Capabilities, each once, each refused where it breaks its definition. - data: the CCMP header the device leaves in front of a decrypted body, A-MSDU with the first-subframe LLC/SNAP refusal of Vanhoef's "Fragment and Forge", and translation to and from Ethernet II by RFC 1042 and 802.1H's bridge tunnel. - reorder: one agreement's receive reordering buffer (10.25.6). HE and WMM are not here: the HE elements are IEEE 802.11ax-2021's and WMM is the Wi-Fi Alliance's, and the brief allows 802.11-2020 and the RFCs alone. Tests: every refusal reached by a hostile input of its own and the set checked against Refused::ALL; boundaries accepted beside them; a seeded structured fuzz of 20,000 frames and 2,000 window runs; and eight captures from tcpdump's test suite, pinned in NOTICE and COMMITTED_FILES. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 4 + Cargo.toml | 1 + NOTICE | 35 +++ build/licence.rs | 49 ++++ lib/ieee80211/Cargo.toml | 7 + lib/ieee80211/src/control.rs | 80 ++++++ lib/ieee80211/src/data.rs | 180 ++++++++++++ lib/ieee80211/src/element/country.rs | 71 +++++ lib/ieee80211/src/element/ht.rs | 132 +++++++++ lib/ieee80211/src/element/mod.rs | 186 ++++++++++++ lib/ieee80211/src/element/rsn.rs | 154 ++++++++++ lib/ieee80211/src/element/tim.rs | 54 ++++ lib/ieee80211/src/frame.rs | 261 +++++++++++++++++ lib/ieee80211/src/lib.rs | 169 +++++++++++ lib/ieee80211/src/mgmt.rs | 128 +++++++++ lib/ieee80211/src/read.rs | 54 ++++ lib/ieee80211/src/reorder.rs | 115 ++++++++ lib/ieee80211/tests/common/mod.rs | 150 ++++++++++ lib/ieee80211/tests/corpus.rs | 133 +++++++++ .../fixtures/tcpdump/ieee802.11_exthdr.pcap | Bin 0 -> 4499 bytes .../fixtures/tcpdump/ieee802.11_htc.pcap | Bin 0 -> 466 bytes .../tcpdump/ieee802.11_meshhdr-oobr.pcap | Bin 0 -> 126 bytes .../fixtures/tcpdump/ieee802.11_meshid.pcap | Bin 0 -> 823 bytes .../ieee802.11_parse_elements_oobr.pcap | Bin 0 -> 295 bytes .../tcpdump/ieee802.11_rates_oobr.pcap | Bin 0 -> 111 bytes .../fixtures/tcpdump/ieee802.11_rx-stbc.pcap | Bin 0 -> 541 bytes .../tcpdump/ieee802.11_tim_ie_oobr.pcap | Bin 0 -> 335 bytes lib/ieee80211/tests/fuzz.rs | 255 +++++++++++++++++ lib/ieee80211/tests/refusals.rs | 264 ++++++++++++++++++ lib/ieee80211/tests/reorder.rs | 107 +++++++ lib/ieee80211/tests/translate.rs | 53 ++++ licenses/BSD-3-Clause-tcpdump.txt | 19 ++ 32 files changed, 2661 insertions(+) create mode 100644 lib/ieee80211/Cargo.toml create mode 100644 lib/ieee80211/src/control.rs create mode 100644 lib/ieee80211/src/data.rs create mode 100644 lib/ieee80211/src/element/country.rs create mode 100644 lib/ieee80211/src/element/ht.rs create mode 100644 lib/ieee80211/src/element/mod.rs create mode 100644 lib/ieee80211/src/element/rsn.rs create mode 100644 lib/ieee80211/src/element/tim.rs create mode 100644 lib/ieee80211/src/frame.rs create mode 100644 lib/ieee80211/src/lib.rs create mode 100644 lib/ieee80211/src/mgmt.rs create mode 100644 lib/ieee80211/src/read.rs create mode 100644 lib/ieee80211/src/reorder.rs create mode 100644 lib/ieee80211/tests/common/mod.rs create mode 100644 lib/ieee80211/tests/corpus.rs create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_htc.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshhdr-oobr.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshid.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rates_oobr.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap create mode 100644 lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap create mode 100644 lib/ieee80211/tests/fuzz.rs create mode 100644 lib/ieee80211/tests/refusals.rs create mode 100644 lib/ieee80211/tests/reorder.rs create mode 100644 lib/ieee80211/tests/translate.rs create mode 100644 licenses/BSD-3-Clause-tcpdump.txt diff --git a/Cargo.lock b/Cargo.lock index b4ee63ff397..5e6d62bb7be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4242,6 +4242,10 @@ dependencies = [ "toyos-device-memory", ] +[[package]] +name = "toyos-ieee80211" +version = "0.1.0" + [[package]] name = "toyos-input" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 79f6d90e898..1998a35a20f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ "lib/fat32/check", "lib/gpt", "lib/hda", + "lib/ieee80211", "lib/input", "lib/inspect", "lib/logkeeper-api", diff --git a/NOTICE b/NOTICE index 9a62e938c13..79bf4a0fe45 100644 --- a/NOTICE +++ b/NOTICE @@ -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. + + kernel/microcode/intel-ucode/* — Intel microcode, redistributable unmodified ---------------------------------------------------------------------------- diff --git a/build/licence.rs b/build/licence.rs index 2e1d9bc24ad..1d0318e65de 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -415,6 +415,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", diff --git a/lib/ieee80211/Cargo.toml b/lib/ieee80211/Cargo.toml new file mode 100644 index 00000000000..07273ea2701 --- /dev/null +++ b/lib/ieee80211/Cargo.toml @@ -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 diff --git a/lib/ieee80211/src/control.rs b/lib/ieee80211/src/control.rs new file mode 100644 index 00000000000..0dd8021f439 --- /dev/null +++ b/lib/ieee80211/src/control.rs @@ -0,0 +1,80 @@ +//! BlockAckReq (9.3.1.7) and BlockAck (9.3.1.8) of the Compressed variant, the only one an HT or +//! VHT station's immediate block-ack agreement uses; their bodies follow Frame Control and +//! Duration. + +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. + pub bitmap: u64, +} + +impl BlockAck { + /// Whether `seq` is inside the bitmap and acknowledged by it. + pub const fn acked(&self, seq: Seq) -> bool { + let offset = seq.since(self.ssn); + offset < 64 && self.bitmap >> offset & 1 != 0 + } +} + +/// The receiver and transmitter, the control field's TID, and the Starting Sequence Control. +fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, 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 ssc = r.u16()?; + // A nonzero Fragment Number in a Compressed BlockAck sizes a bitmap past 64 bits, which the + // HE amendment defines and 802.11-2020 does not. + if ssc & 0x000F != 0 { + return Err(Refused::BitmapSize); + } + Ok((ra, ta, high >> 4, Seq::new(ssc >> 4))) +} + +impl BlockAckReq { + pub(crate) fn parse(after_duration: &[u8]) -> Result { + let mut r = Reader::new(after_duration); + let (ra, ta, tid, ssn) = compressed(&mut r)?; + if !r.is_empty() { + return Err(Refused::ControlLength); + } + Ok(Self { ra, ta, tid, ssn }) + } +} + +impl BlockAck { + pub(crate) fn parse(after_duration: &[u8]) -> Result { + let mut r = Reader::new(after_duration); + let (ra, ta, tid, ssn) = compressed(&mut r)?; + let bitmap = r.u64()?; + if !r.is_empty() { + return Err(Refused::ControlLength); + } + Ok(Self { ra, ta, tid, ssn, bitmap }) + } +} diff --git a/lib/ieee80211/src/data.rs b/lib/ieee80211/src/data.rs new file mode 100644 index 00000000000..5f54bb764bc --- /dev/null +++ b/lib/ieee80211/src/data.rs @@ -0,0 +1,180 @@ +//! Data frame bodies: the CCMP header (12.5.3.2), A-MSDU (9.3.2.2), and translation between an +//! MSDU and an Ethernet II frame by IEEE 802.1H: an RFC 1042 LLC/SNAP header, or the bridge-tunnel +//! one for the two EtherTypes 802.1H sends that way. + +use crate::frame::Data; +use crate::read::Reader; +use crate::{Address, Refused, Seq}; + +/// The CCMP header that stays in front of a frame body its receiver decrypted. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CcmpHeader { + /// The 48-bit packet number. + pub pn: u64, + pub key_id: u8, +} + +impl CcmpHeader { + pub const LEN: usize = 8; + + /// The header, and the body after it. + pub fn split(body: &[u8]) -> Result<(Self, &[u8]), Refused> { + let mut r = Reader::new(body); + let [pn0, pn1, _, key, pn2, pn3, pn4, pn5] = r.array()?; + if key & 1 << 5 == 0 { + return Err(Refused::ExtendedIvClear); + } + let pn = u64::from_le_bytes([pn0, pn1, pn2, pn3, pn4, pn5, 0, 0]); + Ok((Self { pn, key_id: key >> 6 }, r.rest())) + } +} + +/// One MSDU and the addresses it travels between. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Msdu<'a> { + pub da: Address, + pub sa: Address, + pub payload: &'a [u8], +} + +/// The MSDUs `frame` carries in `body`, its plaintext: one, or an A-MSDU's subframes in order. +/// +/// An A-MSDU subframe header is an IEEE 802.3 header, its Length big-endian, and every subframe +/// but the last is padded to a multiple of four octets. +pub fn msdus<'a>(frame: &Data<'_>, body: &'a [u8]) -> Result, Refused> { + if frame.null { + return Ok(Msdus(Inner::One(None))); + } + if !frame.qos.is_some_and(|q| q.amsdu()) { + return Ok(Msdus(Inner::One(Some(Msdu { da: frame.da(), sa: frame.sa(), payload: body })))); + } + // The A-MSDU Present bit is outside CCMP's protection unless both peers negotiated SPP + // A-MSDU, so a first subframe whose destination is an LLC/SNAP header is a frame an attacker + // flipped the bit on (Vanhoef, "Fragment and Forge", USENIX Security 2021, §3.2). + if body.starts_with(&SNAP) { + return Err(Refused::SnapHeaderAsAddress); + } + Ok(Msdus(Inner::Subframes(Reader::new(body)))) +} + +pub struct Msdus<'a>(Inner<'a>); + +enum Inner<'a> { + One(Option>), + Subframes(Reader<'a>), +} + +impl<'a> Iterator for Msdus<'a> { + type Item = Result, Refused>; + + fn next(&mut self) -> Option { + match &mut self.0 { + Inner::One(one) => one.take().map(Ok), + Inner::Subframes(r) if r.is_empty() => None, + Inner::Subframes(r) => { + let next = subframe(r); + if next.is_err() { + *r = Reader::new(&[]); + } + Some(next) + } + } + } +} + +fn subframe<'a>(r: &mut Reader<'a>) -> Result, Refused> { + let da = r.address()?; + let sa = r.address()?; + let len = u16::from_be_bytes(r.array()?); + let payload = r.take(usize::from(len)).map_err(|_| Refused::SubframeLength)?; + if !r.is_empty() { + let pad = usize::from(len.wrapping_add(14).wrapping_neg() & 3); + r.take(pad).map_err(|_| Refused::SubframePadding)?; + if r.is_empty() { + return Err(Refused::SubframePadding); + } + } + Ok(Msdu { da, sa, payload }) +} + +/// An Ethernet II frame's header fields, and its payload. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Ethernet<'a> { + pub dst: Address, + pub src: Address, + pub ethertype: u16, + pub payload: &'a [u8], +} + +const SNAP: [u8; 6] = [0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00]; +const BRIDGE_TUNNEL: [u8; 6] = [0xAA, 0xAA, 0x03, 0x00, 0x00, 0xF8]; +/// The EtherTypes 802.1H carries under the bridge-tunnel OUI: AppleTalk AARP and IPX. +const TUNNELED: [u16; 2] = [0x80F3, 0x8137]; +/// The least EtherType: below it the field is an IEEE 802.3 length (IEEE 802.3 §3.2.6). +const MIN_ETHERTYPE: u16 = 0x0600; + +pub const LLC_SNAP_LEN: usize = 8; + +impl<'a> Ethernet<'a> { + pub fn from_msdu(msdu: Msdu<'a>) -> Result { + let mut r = Reader::new(msdu.payload); + let header: [u8; 6] = r.array()?; + let ethertype = u16::from_be_bytes(r.array()?); + let [llc0, llc1, llc2, ..] = header; + if [llc0, llc1, llc2] != [0xAA, 0xAA, 0x03] { + return Err(Refused::NotSnap); + } + if header == SNAP && TUNNELED.contains(ðertype) { + return Err(Refused::TranslatedType); + } + if header != SNAP && header != BRIDGE_TUNNEL { + return Err(Refused::UnknownOui); + } + if ethertype < MIN_ETHERTYPE { + return Err(Refused::LengthField); + } + Ok(Self { dst: msdu.da, src: msdu.sa, ethertype, payload: r.rest() }) + } +} + +/// The LLC/SNAP header an MSDU carrying `ethertype` starts with. +pub fn llc_snap(ethertype: u16) -> Result<[u8; LLC_SNAP_LEN], Refused> { + if ethertype < MIN_ETHERTYPE { + return Err(Refused::LengthField); + } + let [oui0, oui1, oui2, oui3, oui4, oui5] = if TUNNELED.contains(ðertype) { BRIDGE_TUNNEL } else { SNAP }; + let [t0, t1] = ethertype.to_be_bytes(); + Ok([oui0, oui1, oui2, oui3, oui4, oui5, t0, t1]) +} + +/// The header of a QoS Data frame a station sends its access point: To DS, Address 1 the BSSID, +/// Address 2 the source and Address 3 the destination (Table 9-30). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ToDs { + pub bssid: Address, + pub sa: Address, + pub da: Address, + pub seq: Seq, + pub tid: u8, + pub protected: bool, +} + +impl ToDs { + pub const LEN: usize = 26; + + pub fn header(&self) -> [u8; Self::LEN] { + let mut out = [0; Self::LEN]; + let protected = if self.protected { 0x40 } else { 0 }; + let fields = [0x88, 0x01 | protected, 0, 0] + .into_iter() + .chain(self.bssid.0) + .chain(self.sa.0) + .chain(self.da.0) + .chain((self.seq.get() << 4).to_le_bytes()) + .chain(u16::from(self.tid & 0x0F).to_le_bytes()); + for (slot, byte) in out.iter_mut().zip(fields) { + *slot = byte; + } + out + } +} diff --git a/lib/ieee80211/src/element/country.rs b/lib/ieee80211/src/element/country.rs new file mode 100644 index 00000000000..8948b27acb0 --- /dev/null +++ b/lib/ieee80211/src/element/country.rs @@ -0,0 +1,71 @@ +//! The Country element (9.4.2.8): a country string, then triplets, then a pad octet of 0 where +//! one makes the length even. + +use crate::Refused; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Country<'a> { + /// Two upper-case ISO 3166-1 letters, then the environment or table octet. + pub string: [u8; 3], + triplets: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Triplet { + /// Channels `first_channel` onward, `channels` of them, at no more than `max_power_dbm`. + Subband { first_channel: u8, channels: u8, max_power_dbm: i8 }, + /// The operating class the subband triplets after it are in. + Operating { extension_id: u8, class: u8, coverage_class: u8 }, +} + +/// A first octet of at least this opens an Operating triplet. +const OPERATING_EXTENSION: u8 = 201; +const LAST_CHANNEL: u16 = 200; + +impl<'a> Country<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let Some((&[a, b, environment], rest)) = body.split_first_chunk::<3>() else { + return Err(Refused::ElementLength); + }; + if !a.is_ascii_uppercase() || !b.is_ascii_uppercase() { + return Err(Refused::CountryString); + } + let triplets = match rest.len() % 3 { + 0 => rest, + 1 => match rest.split_last() { + Some((0, triplets)) => triplets, + _ => return Err(Refused::CountryPad), + }, + _ => return Err(Refused::CountryTriplet), + }; + if triplets.is_empty() { + return Err(Refused::ElementLength); + } + for &[first, channels, _] in triplets.as_chunks::<3>().0 { + let past_last = u16::from(first).saturating_add(u16::from(channels)) > LAST_CHANNEL + 1; + if first < OPERATING_EXTENSION && (first == 0 || channels == 0 || past_last) { + return Err(Refused::CountryChannels); + } + } + Ok(Self { string: [a, b, environment], triplets }) + } + + pub fn triplets(&self) -> Triplets<'a> { + Triplets(self.triplets.as_chunks::<3>().0.iter()) + } +} + +pub struct Triplets<'a>(core::slice::Iter<'a, [u8; 3]>); + +impl Iterator for Triplets<'_> { + type Item = Triplet; + + fn next(&mut self) -> Option { + let &[a, b, c] = self.0.next()?; + Some(if a >= OPERATING_EXTENSION { + Triplet::Operating { extension_id: a, class: b, coverage_class: c } + } else { + Triplet::Subband { first_channel: a, channels: b, max_power_dbm: i8::from_le_bytes([c]) } + }) + } +} diff --git a/lib/ieee80211/src/element/ht.rs b/lib/ieee80211/src/element/ht.rs new file mode 100644 index 00000000000..bc11561e509 --- /dev/null +++ b/lib/ieee80211/src/element/ht.rs @@ -0,0 +1,132 @@ +//! HT Capabilities (9.4.2.55), HT Operation (9.4.2.56), VHT Capabilities (9.4.2.157) and VHT +//! Operation (9.4.2.158): fixed-length elements, refused at any other length or where a field +//! holds a value the standard reserves. + +use crate::read::Reader; +use crate::Refused; + +fn exact(body: &[u8]) -> Result<[u8; N], Refused> { + body.try_into().map_err(|_| Refused::ElementLength) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HtCapabilities { + pub info: u16, + pub ampdu_parameters: u8, + /// The Supported MCS Set field; its first 77 bits are the Rx MCS Bitmask. + pub mcs: [u8; 16], + pub extended: u16, + pub beamforming: u32, + pub antenna_selection: u8, +} + +impl HtCapabilities { + pub(super) fn parse(body: &[u8]) -> Result { + let bytes: [u8; 26] = exact(body)?; + let mut r = Reader::new(&bytes); + Ok(Self { + info: r.u16()?, + ampdu_parameters: r.u8()?, + mcs: r.array()?, + extended: r.u16()?, + beamforming: r.u32()?, + antenna_selection: r.u8()?, + }) + } +} + +/// Secondary Channel Offset (9.4.2.56). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SecondaryChannel { + None, + Above, + Below, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HtOperation { + pub primary_channel: u8, + pub secondary: SecondaryChannel, + /// STA Channel Width: any channel width in the HT Capabilities' set. + pub any_width: bool, + pub info: [u8; 5], + pub basic_mcs: [u8; 16], +} + +impl HtOperation { + pub(super) fn parse(body: &[u8]) -> Result { + let bytes: [u8; 22] = exact(body)?; + let mut r = Reader::new(&bytes); + let primary_channel = r.u8()?; + let info: [u8; 5] = r.array()?; + let [first, ..] = info; + let secondary = match first & 0b11 { + 0 => SecondaryChannel::None, + 1 => SecondaryChannel::Above, + 3 => SecondaryChannel::Below, + _ => return Err(Refused::SecondaryChannelOffset), + }; + Ok(Self { primary_channel, secondary, any_width: first & 1 << 2 != 0, info, basic_mcs: r.array()? }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct VhtCapabilities { + pub info: u32, + pub rx_mcs_map: u16, + pub rx_highest: u16, + pub tx_mcs_map: u16, + pub tx_highest: u16, +} + +impl VhtCapabilities { + pub(super) fn parse(body: &[u8]) -> Result { + let bytes: [u8; 12] = exact(body)?; + let mut r = Reader::new(&bytes); + let caps = Self { + info: r.u32()?, + rx_mcs_map: r.u16()?, + rx_highest: r.u16()?, + tx_mcs_map: r.u16()?, + tx_highest: r.u16()?, + }; + if caps.info & 0b11 == 0b11 || (caps.info >> 2) & 0b11 == 0b11 { + return Err(Refused::VhtReserved); + } + Ok(caps) + } +} + +/// The VHT Operation Information's Channel Width (Table 9-274). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VhtChannelWidth { + TwentyOrForty, + /// 80, 160 or 80+80 MHz, told apart by the center frequency segments. + Eighty, + /// 160 MHz, in the deprecated encoding. + OneSixty, + /// Non-contiguous 80+80 MHz, in the deprecated encoding. + EightyPlusEighty, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct VhtOperation { + pub width: VhtChannelWidth, + pub center_segment0: u8, + pub center_segment1: u8, + pub basic_mcs_map: u16, +} + +impl VhtOperation { + pub(super) fn parse(body: &[u8]) -> Result { + let [width, center_segment0, center_segment1, m0, m1] = exact(body)?; + let width = match width { + 0 => VhtChannelWidth::TwentyOrForty, + 1 => VhtChannelWidth::Eighty, + 2 => VhtChannelWidth::OneSixty, + 3 => VhtChannelWidth::EightyPlusEighty, + _ => return Err(Refused::VhtReserved), + }; + Ok(Self { width, center_segment0, center_segment1, basic_mcs_map: u16::from_le_bytes([m0, m1]) }) + } +} diff --git a/lib/ieee80211/src/element/mod.rs b/lib/ieee80211/src/element/mod.rs new file mode 100644 index 00000000000..bbb512a2359 --- /dev/null +++ b/lib/ieee80211/src/element/mod.rs @@ -0,0 +1,186 @@ +//! Elements (9.4.2): the TLV walk of a body's element list, and the elements a station decides +//! by, each checked against its definition and allowed once. +//! +//! An element this module does not type is skipped, as 10.28.9 has a receiver ignore what it +//! does not know; one it types that breaks its definition refuses the whole list. + +mod country; +mod ht; +mod rsn; +mod tim; + +pub use country::{Country, Triplet, Triplets}; +pub use ht::{HtCapabilities, HtOperation, SecondaryChannel, VhtCapabilities, VhtChannelWidth, VhtOperation}; +pub use rsn::{Rsn, Rsnx, Suite, Suites}; +pub use tim::Tim; + +use crate::read::Reader; +use crate::Refused; + +/// An element's identifier: an Element ID, or 255 and its Element ID Extension (9.4.2.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Id { + Base(u8), + Extension(u8), +} + +impl Id { + pub const SSID: Self = Self::Base(0); + pub const SUPPORTED_RATES: Self = Self::Base(1); + pub const DSSS_PARAMETER_SET: Self = Self::Base(3); + pub const TIM: Self = Self::Base(5); + pub const COUNTRY: Self = Self::Base(7); + pub const HT_CAPABILITIES: Self = Self::Base(45); + pub const RSN: Self = Self::Base(48); + pub const EXTENDED_SUPPORTED_RATES: Self = Self::Base(50); + pub const HT_OPERATION: Self = Self::Base(61); + pub const EXTENDED_CAPABILITIES: Self = Self::Base(127); + pub const VHT_CAPABILITIES: Self = Self::Base(191); + pub const VHT_OPERATION: Self = Self::Base(192); + pub const RSNX: Self = Self::Base(244); + const EXTENSION: u8 = 255; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Element<'a> { + pub id: Id, + /// The information, after an extension's Element ID Extension octet. + pub body: &'a [u8], +} + +/// The elements of a list in order; the first malformed one ends it. +pub struct Iter<'a> { + r: Reader<'a>, +} + +pub const fn iter(elements: &[u8]) -> Iter<'_> { + Iter { r: Reader::new(elements) } +} + +impl<'a> Iterator for Iter<'a> { + type Item = Result, Refused>; + + fn next(&mut self) -> Option { + if self.r.is_empty() { + return None; + } + let next = self.read(); + if next.is_err() { + self.r = Reader::new(&[]); + } + Some(next) + } +} + +impl<'a> Iter<'a> { + fn read(&mut self) -> Result, Refused> { + let [id, len] = self.r.array().map_err(|_| Refused::ElementPastBody)?; + let body = self.r.take(usize::from(len)).map_err(|_| Refused::ElementPastBody)?; + if id != Id::EXTENSION { + return Ok(Element { id: Id::Base(id), body }); + } + let (&ext, body) = body.split_first().ok_or(Refused::ExtensionWithoutId)?; + Ok(Element { id: Id::Extension(ext), body }) + } +} + +/// A rate in units of 500 kb/s, or a BSS membership selector (9.4.2.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rate { + /// In the BSSBasicRateSet, or for a selector, required of a member. + pub basic: bool, + pub value: u8, +} + +impl Rate { + /// BSS membership selectors (Table 9-78). + pub const HT_PHY: u8 = 127; + pub const VHT_PHY: u8 = 126; + pub const SAE_H2E_ONLY: u8 = 123; +} + +/// The Supported Rates and BSS Membership Selectors element and its Extended continuation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rates<'a> { + supported: &'a [u8], + extended: &'a [u8], +} + +impl Rates<'_> { + pub fn iter(&self) -> impl Iterator + '_ { + self.supported.iter().chain(self.extended).map(|&b| Rate { basic: b & 0x80 != 0, value: b & 0x7F }) + } +} + +/// The elements of one list a station decides by. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Elements<'a> { + pub ssid: Option<&'a [u8]>, + pub rates: Option>, + pub dsss_channel: Option, + pub tim: Option>, + pub country: Option>, + pub rsn: Option>, + pub rsnx: Option>, + pub ht_capabilities: Option, + pub ht_operation: Option, + pub vht_capabilities: Option, + pub vht_operation: Option, + pub extended_capabilities: Option>, +} + +/// The Extended Capabilities element: bit n of its field, absent past its length, is 0 (9.4.2.26). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExtendedCapabilities<'a>(pub &'a [u8]); + +impl ExtendedCapabilities<'_> { + pub fn bit(&self, n: usize) -> bool { + self.0.get(n / 8).is_some_and(|octet| octet >> (n % 8) & 1 != 0) + } +} + +fn once(slot: &mut Option, value: T) -> Result<(), Refused> { + if slot.is_some() { + return Err(Refused::DuplicateElement); + } + *slot = Some(value); + Ok(()) +} + +impl<'a> Elements<'a> { + pub fn parse(list: &'a [u8]) -> Result { + let mut e = Self::default(); + let mut supported: Option<&'a [u8]> = None; + let mut extended: Option<&'a [u8]> = None; + for element in iter(list) { + let Element { id, body } = element?; + match id { + Id::SSID if body.len() > 32 => return Err(Refused::SsidLength), + Id::SSID => once(&mut e.ssid, body)?, + Id::SUPPORTED_RATES if body.is_empty() || body.len() > 8 => return Err(Refused::RateCount), + Id::SUPPORTED_RATES => once(&mut supported, body)?, + Id::EXTENDED_SUPPORTED_RATES if body.is_empty() => return Err(Refused::ElementLength), + Id::EXTENDED_SUPPORTED_RATES => once(&mut extended, body)?, + Id::DSSS_PARAMETER_SET => match body { + &[channel] => once(&mut e.dsss_channel, channel)?, + _ => return Err(Refused::ElementLength), + }, + Id::TIM => once(&mut e.tim, Tim::parse(body)?)?, + Id::COUNTRY => once(&mut e.country, Country::parse(body)?)?, + Id::RSN => once(&mut e.rsn, Rsn::parse(body)?)?, + Id::RSNX => once(&mut e.rsnx, Rsnx::parse(body)?)?, + Id::HT_CAPABILITIES => once(&mut e.ht_capabilities, HtCapabilities::parse(body)?)?, + Id::HT_OPERATION => once(&mut e.ht_operation, HtOperation::parse(body)?)?, + Id::VHT_CAPABILITIES => once(&mut e.vht_capabilities, VhtCapabilities::parse(body)?)?, + Id::VHT_OPERATION => once(&mut e.vht_operation, VhtOperation::parse(body)?)?, + Id::EXTENDED_CAPABILITIES => once(&mut e.extended_capabilities, ExtendedCapabilities(body))?, + Id::Base(_) | Id::Extension(_) => {} + } + } + e.rates = match (supported, extended) { + (None, None) => None, + (supported, extended) => Some(Rates { supported: supported.unwrap_or(&[]), extended: extended.unwrap_or(&[]) }), + }; + Ok(e) + } +} diff --git a/lib/ieee80211/src/element/rsn.rs b/lib/ieee80211/src/element/rsn.rs new file mode 100644 index 00000000000..42000ffbc22 --- /dev/null +++ b/lib/ieee80211/src/element/rsn.rs @@ -0,0 +1,154 @@ +//! The RSN element (9.4.2.24) and the RSN Extension element (9.4.2.241). +//! +//! An RSNE's fields after its version are each optional, but only from the end: one present +//! needs every one before it. What follows the last field is ignored, as 10.28.9 has a receiver +//! do with an extensible element. + +use crate::read::Reader; +use crate::Refused; + +/// A cipher or AKM suite selector: an OUI or CID and a type (9.4.2.24.2, 9.4.2.24.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Suite(pub [u8; 4]); + +impl Suite { + const fn ieee(kind: u8) -> Self { + Self([0x00, 0x0F, 0xAC, kind]) + } + + // Cipher suites, Table 9-149. + pub const TKIP: Self = Self::ieee(2); + pub const CCMP_128: Self = Self::ieee(4); + pub const BIP_CMAC_128: Self = Self::ieee(6); + pub const GROUP_ADDRESSED_TRAFFIC_NOT_ALLOWED: Self = Self::ieee(7); + pub const GCMP_128: Self = Self::ieee(8); + pub const GCMP_256: Self = Self::ieee(9); + pub const CCMP_256: Self = Self::ieee(10); + pub const BIP_GMAC_128: Self = Self::ieee(11); + pub const BIP_GMAC_256: Self = Self::ieee(12); + pub const BIP_CMAC_256: Self = Self::ieee(13); + + // AKM suites, Table 9-151. + pub const AKM_8021X: Self = Self::ieee(1); + pub const AKM_PSK: Self = Self::ieee(2); + pub const AKM_FT_PSK: Self = Self::ieee(4); + pub const AKM_PSK_SHA256: Self = Self::ieee(6); + pub const AKM_SAE: Self = Self::ieee(8); + pub const AKM_FT_SAE: Self = Self::ieee(9); +} + +/// A list of suite selectors, four octets each. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Suites<'a>(&'a [u8]); + +impl Suites<'_> { + pub fn iter(&self) -> impl Iterator + '_ { + self.0.as_chunks::<4>().0.iter().map(|&s| Suite(s)) + } + + pub fn contains(&self, suite: Suite) -> bool { + self.iter().any(|s| s == suite) + } + + pub const fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rsn<'a> { + pub group_data: Option, + pub pairwise: Option>, + pub akm: Option>, + pub capabilities: Option, + /// The PMKIDs, 16 octets each. + pub pmkids: Option<&'a [u8]>, + pub group_management: Option, +} + +const MFPR: u16 = 1 << 6; +const MFPC: u16 = 1 << 7; + +impl<'a> Rsn<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let mut r = Reader::new(body); + if r.u16().map_err(|_| Refused::ElementLength)? != 1 { + return Err(Refused::RsnVersion); + } + let mut rsn = Self { + group_data: None, + pairwise: None, + akm: None, + capabilities: None, + pmkids: None, + group_management: None, + }; + if r.is_empty() { + return Ok(rsn); + } + rsn.group_data = Some(Suite(r.array()?)); + if r.is_empty() { + return Ok(rsn); + } + rsn.pairwise = Some(Suites(counted(&mut r, 4)?)); + if r.is_empty() { + return Ok(rsn); + } + rsn.akm = Some(Suites(counted(&mut r, 4)?)); + if r.is_empty() { + return Ok(rsn); + } + let capabilities = r.u16()?; + if capabilities & MFPR != 0 && capabilities & MFPC == 0 { + return Err(Refused::MfpRequiredNotCapable); + } + rsn.capabilities = Some(capabilities); + if r.is_empty() { + return Ok(rsn); + } + rsn.pmkids = Some(counted(&mut r, 16)?); + if r.is_empty() { + return Ok(rsn); + } + rsn.group_management = Some(Suite(r.array()?)); + Ok(rsn) + } + + /// Management Frame Protection Capable, bit 7 of the RSN Capabilities field. + pub fn mfp_capable(&self) -> bool { + self.capabilities.is_some_and(|c| c & MFPC != 0) + } + + /// Management Frame Protection Required, bit 6. + pub fn mfp_required(&self) -> bool { + self.capabilities.is_some_and(|c| c & MFPR != 0) + } +} + +/// A two-octet count, then that many items of `size` octets. +fn counted<'a>(r: &mut Reader<'a>, size: usize) -> Result<&'a [u8], Refused> { + let count = usize::from(r.u16()?); + let len = count.checked_mul(size).ok_or(Refused::RsnCount)?; + r.take(len).map_err(|_| Refused::RsnCount) +} + +/// The RSNXE: its Extended RSN Capabilities field, whose low four bits give its length less one. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rsnx<'a>(&'a [u8]); + +impl<'a> Rsnx<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let &first = body.first().ok_or(Refused::ElementLength)?; + let len = usize::from(first & 0x0F).saturating_add(1); + body.get(..len).map(Self).ok_or(Refused::ExtendedRsnLength) + } + + pub fn bit(&self, n: usize) -> bool { + self.0.get(n / 8).is_some_and(|octet| octet >> (n % 8) & 1 != 0) + } + + /// SAE Hash-to-Element, bit 5 (Table 9-321). + pub fn sae_h2e(&self) -> bool { + self.bit(5) + } +} diff --git a/lib/ieee80211/src/element/tim.rs b/lib/ieee80211/src/element/tim.rs new file mode 100644 index 00000000000..42b81de1ab3 --- /dev/null +++ b/lib/ieee80211/src/element/tim.rs @@ -0,0 +1,54 @@ +//! The TIM element (9.4.2.5). + +use crate::read::Reader; +use crate::Refused; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Tim<'a> { + pub dtim_count: u8, + /// Never 0. + pub dtim_period: u8, + control: u8, + bitmap: &'a [u8], +} + +/// The partial virtual bitmap covers AIDs 0 to 2007: octets 0 to 250. +const BITMAP_OCTETS: usize = 251; + +impl<'a> Tim<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let mut r = Reader::new(body); + let [dtim_count, dtim_period, control] = r.array().map_err(|_| Refused::ElementLength)?; + let bitmap = r.rest(); + if bitmap.is_empty() { + return Err(Refused::ElementLength); + } + if dtim_period == 0 { + return Err(Refused::DtimPeriod); + } + let tim = Self { dtim_count, dtim_period, control, bitmap }; + if tim.offset().saturating_add(bitmap.len()) > BITMAP_OCTETS { + return Err(Refused::TimBitmap); + } + Ok(tim) + } + + /// N1, the first octet the partial bitmap holds: the Bitmap Offset subfield times two. + fn offset(&self) -> usize { + usize::from(self.control & 0xFE) + } + + /// Whether group-addressed frames are buffered: bit 0 of the Bitmap Control field. + pub const fn group_buffered(&self) -> bool { + self.control & 1 != 0 + } + + /// Whether frames are buffered for the station with association ID `aid`. + pub fn buffered(&self, aid: u16) -> bool { + let aid = usize::from(aid); + (aid / 8) + .checked_sub(self.offset()) + .and_then(|octet| self.bitmap.get(octet)) + .is_some_and(|octet| octet >> (aid % 8) & 1 != 0) + } +} diff --git a/lib/ieee80211/src/frame.rs b/lib/ieee80211/src/frame.rs new file mode 100644 index 00000000000..6de736db486 --- /dev/null +++ b/lib/ieee80211/src/frame.rs @@ -0,0 +1,261 @@ +//! The MAC header (9.2.3, 9.2.4) of the frames a station handles: every management subtype it +//! takes part in, BlockAckReq and BlockAck, and Data, Null, QoS Data and QoS Null. +//! +//! The input is one MPDU without its FCS. A body is handed back undecoded, because a protected +//! one is ciphertext until the caller removes its protection: management bodies decode in +//! [`crate::mgmt`], data bodies in [`crate::data`]. + +use crate::control::{BlockAck, BlockAckReq}; +use crate::read::Reader; +use crate::{Address, Refused, Seq}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Frame<'a> { + Management(Management<'a>), + BlockAckReq(BlockAckReq), + BlockAck(BlockAck), + Data(Data<'a>), +} + +/// The Frame Control flags a receiver acts on (9.2.4.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Flags { + pub retry: bool, + pub power_management: bool, + pub more_data: bool, + pub protected: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ManagementSubtype { + AssociationRequest, + AssociationResponse, + ReassociationRequest, + ReassociationResponse, + ProbeRequest, + ProbeResponse, + Beacon, + Disassociation, + Authentication, + Deauthentication, + Action, + ActionNoAck, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Management<'a> { + pub flags: Flags, + pub subtype: ManagementSubtype, + pub duration: u16, + pub da: Address, + pub sa: Address, + pub bssid: Address, + pub seq: Seq, + pub ht_control: Option, + pub body: &'a [u8], +} + +/// To DS and From DS (Table 9-4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ds { + /// Station to station within one BSS. + Neither, + /// Station to access point. + ToDs, + /// Access point to station. + FromDs, + /// The four-address format, with its Address 4. + Both(Address), +} + +/// The QoS Control field (9.2.4.5). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct QosControl(pub u16); + +impl QosControl { + pub const fn tid(self) -> u8 { + let [low, _] = self.0.to_le_bytes(); + low & 0x0F + } + + /// The Ack Policy subfield, bits 5 and 6 (Table 9-11). + pub const fn ack_policy(self) -> u8 { + let [low, _] = self.0.to_le_bytes(); + (low >> 5) & 0b11 + } + + /// The A-MSDU Present subfield, bit 7. + pub const fn amsdu(self) -> bool { + self.0 & 0x0080 != 0 + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Data<'a> { + pub flags: Flags, + pub ds: Ds, + pub duration: u16, + pub addr1: Address, + pub addr2: Address, + pub addr3: Address, + pub seq: Seq, + pub qos: Option, + pub ht_control: Option, + /// A Null or QoS Null frame, which carries no MSDU. + pub null: bool, + pub body: &'a [u8], +} + +impl Data<'_> { + /// The receiver, which Address 1 always is. + pub const fn ra(&self) -> Address { + self.addr1 + } + + /// The transmitter, which Address 2 always is. + pub const fn ta(&self) -> Address { + self.addr2 + } + + /// The MSDU's destination (Table 9-30). + pub const fn da(&self) -> Address { + match self.ds { + Ds::Neither | Ds::FromDs => self.addr1, + Ds::ToDs | Ds::Both(_) => self.addr3, + } + } + + /// The MSDU's source (Table 9-30). + pub const fn sa(&self) -> Address { + match self.ds { + Ds::Neither | Ds::ToDs => self.addr2, + Ds::FromDs => self.addr3, + Ds::Both(addr4) => addr4, + } + } +} + +impl<'a> Frame<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + let mut r = Reader::new(bytes); + let [fc0, fc1] = r.array()?; + if fc0 & 0b11 != 0 { + return Err(Refused::ProtocolVersion); + } + let subtype = fc0 >> 4; + let flags = Flags { + retry: fc1 & 0x08 != 0, + power_management: fc1 & 0x10 != 0, + more_data: fc1 & 0x20 != 0, + protected: fc1 & 0x40 != 0, + }; + let ds_bits = fc1 & 0b11; + let more_fragments = fc1 & 0x04 != 0; + let order = fc1 & 0x80 != 0; + let duration = r.u16()?; + match (fc0 >> 2) & 0b11 { + 0 => { + let subtype = management_subtype(subtype)?; + if ds_bits != 0 { + return Err(Refused::ManagementWithDs); + } + let da = r.address()?; + let sa = r.address()?; + let bssid = r.address()?; + let seq = sequence_control(r.u16()?, more_fragments)?; + if sa.is_group() { + return Err(Refused::GroupTransmitter); + } + let ht_control = if order { Some(r.u32()?) } else { None }; + Ok(Self::Management(Management { + flags, + subtype, + duration, + da, + sa, + bssid, + seq, + ht_control, + body: r.rest(), + })) + } + 1 => match subtype { + 8 => BlockAckReq::parse(r.rest()).map(Self::BlockAckReq), + 9 => BlockAck::parse(r.rest()).map(Self::BlockAck), + _ => Err(Refused::UnhandledControl), + }, + 2 => { + let (qos, null) = match subtype { + 0 => (false, false), + 4 => (false, true), + 8 => (true, false), + 12 => (true, true), + 13 => return Err(Refused::ReservedSubtype), + _ => return Err(Refused::ContentionFreeData), + }; + if order && !qos { + return Err(Refused::StrictlyOrdered); + } + let addr1 = r.address()?; + let addr2 = r.address()?; + let addr3 = r.address()?; + let seq = sequence_control(r.u16()?, more_fragments)?; + let ds = match ds_bits { + 0 => Ds::Neither, + 1 => Ds::ToDs, + 2 => Ds::FromDs, + _ => Ds::Both(r.address()?), + }; + if addr2.is_group() { + return Err(Refused::GroupTransmitter); + } + let qos = if qos { Some(QosControl(r.u16()?)) } else { None }; + let ht_control = if order { Some(r.u32()?) } else { None }; + Ok(Self::Data(Data { + flags, + ds, + duration, + addr1, + addr2, + addr3, + seq, + qos, + ht_control, + null, + body: r.rest(), + })) + } + _ => Err(Refused::ExtensionFrame), + } + } +} + +/// Table 9-1. Timing Advertisement and ATIM are no station's business in an infrastructure BSS. +fn management_subtype(subtype: u8) -> Result { + use ManagementSubtype::*; + Ok(match subtype { + 0 => AssociationRequest, + 1 => AssociationResponse, + 2 => ReassociationRequest, + 3 => ReassociationResponse, + 4 => ProbeRequest, + 5 => ProbeResponse, + 8 => Beacon, + 10 => Disassociation, + 11 => Authentication, + 12 => Deauthentication, + 13 => Action, + 14 => ActionNoAck, + 7 | 15 => return Err(Refused::ReservedSubtype), + _ => return Err(Refused::UnhandledManagement), + }) +} + +/// A fragment is refused rather than reassembled: no reassembly buffer exists to mix fragments +/// of different keys or frames in (Vanhoef, "Fragment and Forge", USENIX Security 2021, §5). +fn sequence_control(field: u16, more_fragments: bool) -> Result { + if more_fragments || field & 0x000F != 0 { + return Err(Refused::Fragmented); + } + Ok(Seq::new(field >> 4)) +} diff --git a/lib/ieee80211/src/lib.rs b/lib/ieee80211/src/lib.rs new file mode 100644 index 00000000000..8239372a201 --- /dev/null +++ b/lib/ieee80211/src/lib.rs @@ -0,0 +1,169 @@ +//! IEEE Std 802.11-2020 frames as a station meets them: parsed in place from bytes a radio or a +//! peer wrote, and refused by the first rule they break, by name. +//! +//! [`frame::Frame::parse`] takes one MPDU without its FCS and yields a typed management, control +//! or data frame whose body still borrows the input. Elements are read by [`element`], A-MSDU +//! and the translation to and from Ethernet by [`data`], and the receive reordering buffer is +//! [`reorder::Window`]. +//! +//! A [`Refused`] is either [`Class::Malformed`], input no correct sender produces, or +//! [`Class::Unsupported`], a well-formed frame this station does not handle. + +#![no_std] +#![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] + +extern crate alloc; + +pub mod control; +pub mod data; +pub mod element; +pub mod frame; +pub mod mgmt; +pub mod reorder; + +mod read; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Class { + /// No correct sender produces these bytes. + Malformed, + /// Well formed, and not something this station handles. + Unsupported, +} + +macro_rules! refusals { + ($($variant:ident = $class:ident, $why:literal;)*) => { + /// Why a frame, element or sequence number was not accepted. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum Refused { + $(#[doc = $why] $variant,)* + } + + impl Refused { + pub const ALL: &'static [Self] = &[$(Self::$variant,)*]; + + pub const fn class(self) -> Class { + match self { + $(Self::$variant => Class::$class,)* + } + } + + pub const fn reason(self) -> &'static str { + match self { + $(Self::$variant => $why,)* + } + } + } + }; +} + +refusals! { + // The MAC header, 9.2. + Truncated = Malformed, "the bytes end inside a field"; + ProtocolVersion = Unsupported, "the protocol version is not 0 (9.2.4.1.2)"; + ExtensionFrame = Unsupported, "a frame of type Extension (DMG and S1G only)"; + ReservedSubtype = Malformed, "a subtype the standard reserves"; + ManagementWithDs = Malformed, "a management frame with To DS or From DS set"; + UnhandledManagement = Unsupported, "a management subtype a station does not handle"; + UnhandledControl = Unsupported, "a control frame other than BlockAckReq or BlockAck"; + ContentionFreeData = Unsupported, "a data subtype of the point coordination function"; + StrictlyOrdered = Unsupported, "a non-QoS data frame with the Order bit set"; + Fragmented = Unsupported, "a fragment of an MSDU or MMPDU"; + GroupTransmitter = Malformed, "a transmitter address with the group bit set"; + // Management bodies, 9.3.3. + ZeroBeaconInterval = Malformed, "a beacon interval of 0 time units"; + AssociationId = Malformed, "an association ID outside 1 to 2007 in a successful response"; + // Elements, 9.4.2. + ElementPastBody = Malformed, "an element's length runs past the frame body"; + ExtensionWithoutId = Malformed, "an Element ID Extension element of length 0"; + DuplicateElement = Malformed, "an element that may appear once appears twice"; + ElementLength = Malformed, "an element whose length its definition does not allow"; + SsidLength = Malformed, "an SSID longer than 32 octets"; + RateCount = Malformed, "a Supported Rates element of 0 or more than 8 rates"; + DtimPeriod = Malformed, "a TIM element with a DTIM period of 0"; + TimBitmap = Malformed, "a TIM partial virtual bitmap past AID 2007"; + CountryString = Malformed, "a country string whose first two octets are not upper-case letters"; + CountryPad = Malformed, "a Country element whose pad octet is not 0"; + CountryTriplet = Malformed, "a Country element whose triplets do not tile it"; + CountryChannels = Malformed, "a subband triplet of 0 channels, or running past channel 200"; + RsnVersion = Unsupported, "an RSN element of a version other than 1"; + RsnCount = Malformed, "an RSN suite count running past the element"; + MfpRequiredNotCapable = Malformed, "management frame protection required but not capable"; + ExtendedRsnLength = Malformed, "an RSNXE whose field length runs past the element"; + SecondaryChannelOffset = Malformed, "an HT Operation secondary channel offset of 2"; + VhtReserved = Malformed, "a VHT capability or operation field of a reserved value"; + // Control frames, 9.3.1. + BlockAckVariant = Unsupported, "a block-ack variant other than Compressed"; + BitmapSize = Unsupported, "a compressed bitmap other than 64 bits"; + ControlLength = Malformed, "a block-ack frame whose length its variant does not give"; + // Data, 9.3.2 and 12.5.3. + SubframeLength = Malformed, "an A-MSDU subframe whose length runs past the MPDU"; + SubframePadding = Malformed, "an A-MSDU subframe whose padding runs past the MPDU"; + SnapHeaderAsAddress = Malformed, "an A-MSDU whose first destination is an LLC/SNAP header"; + NotSnap = Unsupported, "an MSDU that is not LLC/SNAP"; + UnknownOui = Unsupported, "an LLC/SNAP header with an OUI other than RFC 1042's or bridge-tunnel's"; + TranslatedType = Malformed, "an RFC 1042 header carrying a type 802.1H sends by bridge-tunnel"; + LengthField = Unsupported, "an EtherType below 0x0600, which is an IEEE 802.3 length"; + ExtendedIvClear = Malformed, "a CCMP header without its Ext IV bit"; + // The reordering buffer, 10.25.6. + WindowSize = Malformed, "a reordering buffer of 0 or more than 64 MPDUs"; + OldSequence = Malformed, "a sequence number before the window"; + DuplicateSequence = Malformed, "a sequence number already held in the window"; +} + +impl core::fmt::Display for Refused { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.reason()) + } +} + +/// A 48-bit MAC address (IEEE 802-2014 §8). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Address(pub [u8; 6]); + +impl Address { + pub const BROADCAST: Self = Self([0xFF; 6]); + + /// The Individual/Group bit, the least significant bit of the first octet. + pub const fn is_group(self) -> bool { + let [first, ..] = self.0; + first & 1 != 0 + } +} + +/// A 12-bit sequence number, compared modulo 4096 (9.2.4.4.2, 10.3.2.14). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Seq(u16); + +impl Seq { + pub const MODULUS: u16 = 4096; + + /// The low 12 bits of `value`. + pub const fn new(value: u16) -> Self { + Self(value & 0x0FFF) + } + + pub const fn get(self) -> u16 { + self.0 + } + + pub const fn add(self, n: u16) -> Self { + Self::new(self.0.wrapping_add(n)) + } + + /// How far `self` is ahead of `base`, modulo 4096. + pub const fn since(self, base: Self) -> u16 { + self.0.wrapping_sub(base.0) & 0x0FFF + } +} diff --git a/lib/ieee80211/src/mgmt.rs b/lib/ieee80211/src/mgmt.rs new file mode 100644 index 00000000000..e095c1e9473 --- /dev/null +++ b/lib/ieee80211/src/mgmt.rs @@ -0,0 +1,128 @@ +//! Management frame bodies (9.3.3): the fixed fields of each subtype, and its elements still as +//! bytes for [`crate::element::Elements::parse`]. +//! +//! An Authentication frame's fields past its status, an SAE exchange's among them, and an Action +//! frame's past its category are its algorithm's or category's to read. + +use crate::frame::ManagementSubtype; +use crate::read::Reader; +use crate::{Address, Refused}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Body<'a> { + Beacon(Beacon<'a>), + ProbeResponse(Beacon<'a>), + ProbeRequest { elements: &'a [u8] }, + Authentication(Authentication<'a>), + Deauthentication { reason: u16, rest: &'a [u8] }, + Disassociation { reason: u16, rest: &'a [u8] }, + AssociationRequest(AssociationRequest<'a>), + ReassociationRequest(AssociationRequest<'a>), + AssociationResponse(AssociationResponse<'a>), + ReassociationResponse(AssociationResponse<'a>), + Action { category: u8, rest: &'a [u8] }, + ActionNoAck { category: u8, rest: &'a [u8] }, +} + +/// A Beacon or Probe Response (9.3.3.2, 9.3.3.10). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Beacon<'a> { + pub timestamp: u64, + /// In time units of 1,024 µs, never 0. + pub interval: u16, + pub capability: u16, + pub elements: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Authentication<'a> { + pub algorithm: u16, + pub transaction: u16, + pub status: u16, + pub rest: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AssociationRequest<'a> { + pub capability: u16, + pub listen_interval: u16, + /// A reassociation's Current AP Address. + pub current_ap: Option
, + pub elements: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AssociationResponse<'a> { + pub capability: u16, + pub status: u16, + /// 1 to 2007 when the status is success, and absent otherwise. + pub aid: Option, + pub elements: &'a [u8], +} + +/// The Authentication Algorithm Number of SAE (Table 9-43). +pub const ALGORITHM_SAE: u16 = 3; +pub const ALGORITHM_OPEN: u16 = 0; +pub const STATUS_SUCCESS: u16 = 0; + +impl<'a> Body<'a> { + pub fn parse(subtype: ManagementSubtype, body: &'a [u8]) -> Result { + use ManagementSubtype as S; + let mut r = Reader::new(body); + Ok(match subtype { + S::Beacon => Self::Beacon(Beacon::read(&mut r)?), + S::ProbeResponse => Self::ProbeResponse(Beacon::read(&mut r)?), + S::ProbeRequest => Self::ProbeRequest { elements: body }, + S::Authentication => Self::Authentication(Authentication { + algorithm: r.u16()?, + transaction: r.u16()?, + status: r.u16()?, + rest: r.rest(), + }), + S::Deauthentication => Self::Deauthentication { reason: r.u16()?, rest: r.rest() }, + S::Disassociation => Self::Disassociation { reason: r.u16()?, rest: r.rest() }, + S::AssociationRequest => Self::AssociationRequest(AssociationRequest::read(&mut r, false)?), + S::ReassociationRequest => Self::ReassociationRequest(AssociationRequest::read(&mut r, true)?), + S::AssociationResponse => Self::AssociationResponse(AssociationResponse::read(&mut r)?), + S::ReassociationResponse => Self::ReassociationResponse(AssociationResponse::read(&mut r)?), + S::Action => Self::Action { category: r.u8()?, rest: r.rest() }, + S::ActionNoAck => Self::ActionNoAck { category: r.u8()?, rest: r.rest() }, + }) + } +} + +impl<'a> Beacon<'a> { + fn read(r: &mut Reader<'a>) -> Result { + let timestamp = r.u64()?; + let interval = r.u16()?; + let capability = r.u16()?; + if interval == 0 { + return Err(Refused::ZeroBeaconInterval); + } + Ok(Self { timestamp, interval, capability, elements: r.rest() }) + } +} + +impl<'a> AssociationRequest<'a> { + fn read(r: &mut Reader<'a>, reassociation: bool) -> Result { + let capability = r.u16()?; + let listen_interval = r.u16()?; + let current_ap = if reassociation { Some(r.address()?) } else { None }; + Ok(Self { capability, listen_interval, current_ap, elements: r.rest() }) + } +} + +impl<'a> AssociationResponse<'a> { + fn read(r: &mut Reader<'a>) -> Result { + let capability = r.u16()?; + let status = r.u16()?; + // The two most significant bits of the AID field are set (9.4.1.8). + let aid = r.u16()? & 0x3FFF; + let aid = match status { + STATUS_SUCCESS if (1..=2007).contains(&aid) => Some(aid), + STATUS_SUCCESS => return Err(Refused::AssociationId), + _ => None, + }; + Ok(Self { capability, status, aid, elements: r.rest() }) + } +} diff --git a/lib/ieee80211/src/read.rs b/lib/ieee80211/src/read.rs new file mode 100644 index 00000000000..d7152178178 --- /dev/null +++ b/lib/ieee80211/src/read.rs @@ -0,0 +1,54 @@ +//! A cursor over untrusted bytes: every read either yields the field or [`Refused::Truncated`]. + +use crate::{Address, Refused}; + +pub(crate) struct Reader<'a> { + rest: &'a [u8], +} + +impl<'a> Reader<'a> { + pub(crate) const fn new(bytes: &'a [u8]) -> Self { + Self { rest: bytes } + } + + pub(crate) const fn rest(&self) -> &'a [u8] { + self.rest + } + + pub(crate) const fn is_empty(&self) -> bool { + self.rest.is_empty() + } + + pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], Refused> { + let (head, tail) = self.rest.split_at_checked(n).ok_or(Refused::Truncated)?; + self.rest = tail; + Ok(head) + } + + pub(crate) fn array(&mut self) -> Result<[u8; N], Refused> { + let (head, tail) = self.rest.split_first_chunk::().ok_or(Refused::Truncated)?; + self.rest = tail; + Ok(*head) + } + + pub(crate) fn u8(&mut self) -> Result { + let [b] = self.array()?; + Ok(b) + } + + pub(crate) fn u16(&mut self) -> Result { + self.array().map(u16::from_le_bytes) + } + + pub(crate) fn u32(&mut self) -> Result { + self.array().map(u32::from_le_bytes) + } + + pub(crate) fn u64(&mut self) -> Result { + self.array().map(u64::from_le_bytes) + } + + pub(crate) fn address(&mut self) -> Result { + self.array().map(Address) + } +} diff --git a/lib/ieee80211/src/reorder.rs b/lib/ieee80211/src/reorder.rs new file mode 100644 index 00000000000..2b4fd13d9e9 --- /dev/null +++ b/lib/ieee80211/src/reorder.rs @@ -0,0 +1,115 @@ +//! The receive reordering buffer of one block-ack agreement (10.25.6): MPDUs held by sequence +//! number and passed up in order. +//! +//! A frame in the window is held, and the run from the window's start passes up. A frame past +//! the window's end, within half the sequence space, moves the window to end at it, passing up +//! everything held before the new start. One further back than that is old and refused. A +//! BlockAckReq's Starting Sequence Number moves the start forward as a frame would, and so does +//! a device's release or a timeout, through [`Window::release_to`]. + +use alloc::vec::Vec; +use core::num::{NonZeroU16, NonZeroUsize}; + +use crate::{Refused, Seq}; + +/// The largest buffer an 802.11-2020 HT or VHT agreement negotiates (9.4.1.14). +pub const MAX_SIZE: u16 = 64; +const HALF: u16 = Seq::MODULUS / 2; + +pub struct Window { + start: Seq, + /// Slot `(head + n) % size` holds sequence number `start + n`. + slots: Vec>, + size: NonZeroU16, + head: usize, +} + +impl Window { + /// A window of `size` MPDUs starting at the agreement's Starting Sequence Number. + pub fn new(ssn: Seq, size: u16) -> Result { + let Some(size) = NonZeroU16::new(size).filter(|s| s.get() <= MAX_SIZE) else { + return Err(Refused::WindowSize); + }; + let mut slots = Vec::new(); + slots.resize_with(usize::from(size.get()), || None); + Ok(Self { start: ssn, slots, size, head: 0 }) + } + + pub const fn start(&self) -> Seq { + self.start + } + + /// Take `item`, received with sequence number `seq`, and pass up into `out` every item it + /// puts in order. + pub fn receive(&mut self, seq: Seq, item: T, out: &mut Vec) -> Result<(), Refused> { + let ahead = seq.since(self.start); + if ahead >= HALF { + return Err(Refused::OldSequence); + } + // A frame past the window's end becomes its end: the start moves to `size - 1` before it. + if let Some(past) = ahead.checked_sub(self.size.get().saturating_sub(1)).filter(|&p| p > 0) { + self.advance(past, out); + } + let offset = usize::from(seq.since(self.start)); + let slot = self.slot(offset); + if slot.is_some() { + return Err(Refused::DuplicateSequence); + } + *slot = Some(item); + self.pass_run(out); + Ok(()) + } + + /// Move the window's start forward to `ssn`, passing up every held item before it, then the + /// run from it. A `ssn` at or behind the start changes nothing. + pub fn release_to(&mut self, ssn: Seq, out: &mut Vec) { + let ahead = ssn.since(self.start); + if ahead == 0 || ahead >= HALF { + return; + } + self.advance(ahead, out); + self.pass_run(out); + } + + /// The sequence number of the first item held, which a timeout releases to. + pub fn first_held(&self) -> Option { + (0..self.slots.len()) + .find(|&n| self.peek(n)) + .and_then(|n| u16::try_from(n).ok()) + .map(|n| self.start.add(n)) + } + + fn index(&self, offset: usize) -> usize { + self.head.wrapping_add(offset) % NonZeroUsize::from(self.size) + } + + fn peek(&self, offset: usize) -> bool { + self.slots.get(self.index(offset)).is_some_and(Option::is_some) + } + + fn slot(&mut self, offset: usize) -> &mut Option { + let index = self.index(offset); + match self.slots.get_mut(index) { + Some(slot) => slot, + None => unreachable!("an index modulo the slot count is a slot"), + } + } + + /// Move the start `n` forward, passing up what the slots it leaves hold. + fn advance(&mut self, n: u16, out: &mut Vec) { + for _ in 0..usize::from(n).min(self.slots.len()) { + out.extend(self.slot(0).take()); + self.head = self.index(1); + } + self.start = self.start.add(n); + } + + /// Pass up the run of held items from the start. + fn pass_run(&mut self, out: &mut Vec) { + while let Some(item) = self.slot(0).take() { + out.push(item); + self.head = self.index(1); + self.start = self.start.add(1); + } + } +} diff --git a/lib/ieee80211/tests/common/mod.rs b/lib/ieee80211/tests/common/mod.rs new file mode 100644 index 00000000000..dcef25bab37 --- /dev/null +++ b/lib/ieee80211/tests/common/mod.rs @@ -0,0 +1,150 @@ +#![allow(dead_code)] + +use toyos_ieee80211::Address; + +/// SplitMix64: a seeded stream, so a fuzz red names its iteration. +pub struct Rng(pub u64); + +impl Rng { + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + pub fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } + + pub fn chance(&mut self, percent: usize) -> bool { + self.below(100) < percent + } + + pub fn bytes(&mut self, max: usize) -> Vec { + let len = self.below(max + 1); + (0..len).map(|_| self.next() as u8).collect() + } + + pub fn address(&mut self) -> Address { + let mut a = [0; 6]; + a.iter_mut().for_each(|b| *b = self.next() as u8); + a[0] &= !1; + Address(a) + } +} + +pub const AP: Address = Address([0x02, 0x11, 0x22, 0x33, 0x44, 0x55]); +pub const STA: Address = Address([0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE]); +pub const PEER: Address = Address([0x02, 0x01, 0x02, 0x03, 0x04, 0x05]); + +/// A management frame's header: Frame Control's subtype, then DA, SA and BSSID. +pub fn management(subtype: u8, body: &[u8]) -> Vec { + let mut f = vec![subtype << 4, 0, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend(AP.0); + f.extend([0x10, 0x00]); + f.extend(body); + f +} + +/// An element: its ID, its length, its body. +pub fn element(id: u8, body: &[u8]) -> Vec { + let mut e = vec![id, body.len() as u8]; + e.extend(body); + e +} + +/// A Beacon body around `elements`. +pub fn beacon(elements: &[u8]) -> Vec { + let mut b = vec![0; 8]; + b.extend(100u16.to_le_bytes()); + b.extend(0x0431u16.to_le_bytes()); + b.extend(elements); + b +} + +/// A QoS Data frame from the access point, its A-MSDU Present bit as given. +pub fn qos_from_ds(amsdu: bool, body: &[u8]) -> Vec { + let mut f = vec![0x88, 0x02, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend(PEER.0); + f.extend([0x20, 0x00]); + f.extend([if amsdu { 0x80 } else { 0 }, 0]); + f.extend(body); + f +} + +/// An A-MSDU subframe, padded to four octets unless it is the last. +pub fn subframe(da: Address, sa: Address, msdu: &[u8], last: bool) -> Vec { + let mut s = Vec::new(); + s.extend(da.0); + s.extend(sa.0); + s.extend((msdu.len() as u16).to_be_bytes()); + s.extend(msdu); + while !last && s.len() % 4 != 0 { + s.push(0); + } + s +} + +/// One record of a pcap file: its link type and the bytes captured. +pub struct Record { + pub linktype: u32, + pub bytes: Vec, +} + +/// The records of a classic pcap file (draft-ietf-opsawg-pcap), either byte order. +pub fn pcap(file: &[u8]) -> Vec { + let magic = u32::from_le_bytes(file[..4].try_into().unwrap()); + let le = match magic { + 0xa1b2_c3d4 | 0xa1b2_3c4d => true, + 0xd4c3_b2a1 | 0x4d3c_b2a1 => false, + _ => panic!("not a pcap file: magic {magic:#x}"), + }; + let word = |at: usize| { + let b: [u8; 4] = file[at..at + 4].try_into().unwrap(); + if le { u32::from_le_bytes(b) } else { u32::from_be_bytes(b) } + }; + let linktype = word(20) & 0x0FFF_FFFF; + let mut out = Vec::new(); + let mut at = 24; + while at < file.len() { + let len = word(at + 8) as usize; + out.push(Record { linktype, bytes: file[at + 16..at + 16 + len].to_vec() }); + at += 16 + len; + } + out +} + +/// The 802.11 frame a record holds, without a radiotap header (radiotap.org) and without an FCS: +/// a trailing four octets that are the CRC-32 of what precedes them are the FCS. +pub fn mpdu(record: &Record) -> Vec { + let frame = match record.linktype & 0xFFFF { + 105 => &record.bytes[..], + 127 => { + let len = u16::from_le_bytes([record.bytes[2], record.bytes[3]]) as usize; + &record.bytes[len.min(record.bytes.len())..] + } + other => panic!("link type {other} is not 802.11"), + }; + match frame.split_last_chunk::<4>() { + Some((body, fcs)) if crc32(body) == u32::from_le_bytes(*fcs) => body.to_vec(), + _ => frame.to_vec(), + } +} + +/// CRC-32 of IEEE 802.3 §3.2.9, which 802.11's FCS is (9.2.4.8). +pub fn crc32(bytes: &[u8]) -> u32 { + let mut crc = !0u32; + for &b in bytes { + crc ^= u32::from(b); + for _ in 0..8 { + crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 }; + } + } + !crc +} diff --git a/lib/ieee80211/tests/corpus.rs b/lib/ieee80211/tests/corpus.rs new file mode 100644 index 00000000000..a1f608b90fe --- /dev/null +++ b/lib/ieee80211/tests/corpus.rs @@ -0,0 +1,133 @@ +//! Frames other implementations wrote, from tcpdump's test captures (`fixtures/tcpdump/`, pinned in +//! `NOTICE`), each read as the standard says its bytes read. + +mod common; + +use toyos_ieee80211::data::{msdus, CcmpHeader, Ethernet}; +use toyos_ieee80211::element::{Elements, SecondaryChannel, Suite, VhtChannelWidth}; +use toyos_ieee80211::frame::{Data, Frame, ManagementSubtype}; +use toyos_ieee80211::mgmt::Body; +use toyos_ieee80211::Address; + +fn frames(name: &str) -> Vec> { + let path = format!("{}/tests/fixtures/tcpdump/{name}.pcap", env!("CARGO_MANIFEST_DIR")); + let file = std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}")); + common::pcap(&file).iter().map(common::mpdu).collect() +} + +/// A frame's outcome in a word: its kind, or its own or its body's or elements' refusal. +fn outcome(mpdu: &[u8]) -> String { + match Frame::parse(mpdu) { + Err(e) => format!("{e:?}"), + Ok(Frame::Data(d)) => format!("{}Data", if d.null { "Null" } else { "" }), + Ok(Frame::BlockAck(_) | Frame::BlockAckReq(_)) => "BlockAck".into(), + Ok(Frame::Management(m)) => { + let elements = match Body::parse(m.subtype, m.body) { + Err(e) => return format!("{:?}:{e:?}", m.subtype), + Ok(Body::Beacon(b) | Body::ProbeResponse(b)) => Some(b.elements), + Ok(Body::ProbeRequest { elements }) => Some(elements), + Ok(Body::AssociationRequest(a) | Body::ReassociationRequest(a)) => Some(a.elements), + Ok(Body::AssociationResponse(a) | Body::ReassociationResponse(a)) => Some(a.elements), + Ok(_) => None, + }; + match elements.map(Elements::parse) { + Some(Err(e)) => format!("{:?}:{e:?}", m.subtype), + _ => format!("{:?}", m.subtype), + } + } + } +} + +fn data(mpdu: &[u8]) -> Data<'_> { + match Frame::parse(mpdu) { + Ok(Frame::Data(d)) => d, + other => panic!("not a data frame: {other:?}"), + } +} + +#[test] +fn every_capture_frame_reads_as_its_bytes_say_or_is_refused_by_name() { + let mut seen = Vec::new(); + for name in [ + "ieee802.11_exthdr", + "ieee802.11_htc", + "ieee802.11_meshid", + "ieee802.11_rx-stbc", + "ieee802.11_parse_elements_oobr", + "ieee802.11_rates_oobr", + "ieee802.11_tim_ie_oobr", + "ieee802.11_meshhdr-oobr", + ] { + seen.push(format!("{name}: {}", frames(name).iter().map(|f| outcome(f)).collect::>().join(" "))); + } + let exthdr_scan = "ProbeRequest UnhandledControl ProbeResponse ".repeat(6); + let expected = [ + format!( + "ieee802.11_exthdr: {exthdr_scan}Authentication UnhandledControl Authentication AssociationRequest \ + UnhandledControl AssociationResponse NullData NullData" + ), + "ieee802.11_htc: Data".into(), + "ieee802.11_meshid: Beacon ProbeRequest ProbeResponse".into(), + "ieee802.11_rx-stbc: Data Data Data".into(), + "ieee802.11_parse_elements_oobr: Beacon:RsnVersion".into(), + "ieee802.11_rates_oobr: ProtocolVersion".into(), + "ieee802.11_tim_ie_oobr: ReassociationResponse:RsnVersion ReassociationResponse:ElementPastBody \ + Truncated ReassociationResponse:ElementPastBody" + .into(), + "ieee802.11_meshhdr-oobr: ProtocolVersion".into(), + ]; + assert_eq!(seen, expected); +} + +#[test] +fn a_captured_sae_beacon_reads_its_rsn_ht_and_vht_operation() { + let beacon = &frames("ieee802.11_meshid")[0]; + let Ok(Frame::Management(m)) = Frame::parse(beacon) else { panic!("not management") }; + assert_eq!(m.subtype, ManagementSubtype::Beacon); + let Ok(Body::Beacon(b)) = Body::parse(m.subtype, m.body) else { panic!("not a beacon body") }; + let e = Elements::parse(b.elements).unwrap(); + + assert_eq!(e.dsss_channel, Some(149)); + let rsn = e.rsn.unwrap(); + assert_eq!(rsn.group_data, Some(Suite::CCMP_128)); + assert!(rsn.pairwise.unwrap().contains(Suite::CCMP_128)); + assert!(rsn.akm.unwrap().contains(Suite::AKM_SAE)); + assert!(!rsn.mfp_capable()); + let ht = e.ht_operation.unwrap(); + assert_eq!((ht.primary_channel, ht.secondary), (149, SecondaryChannel::Above)); + let vht = e.vht_operation.unwrap(); + // Channels 149 to 161 are one 80 MHz channel, centred on 155. + assert_eq!((vht.width, vht.center_segment0, vht.center_segment1), (VhtChannelWidth::Eighty, 155, 0)); + let tim = e.tim.unwrap(); + assert_eq!((tim.dtim_count, tim.dtim_period, tim.group_buffered()), (1, 2, false)); +} + +#[test] +fn a_captured_ccmp_frame_keeps_its_header_and_carries_an_ipv4_msdu() { + let frame = &frames("ieee802.11_rx-stbc")[0]; + let d = data(frame); + assert!(d.flags.protected); + let (ccmp, plaintext) = CcmpHeader::split(d.body).unwrap(); + assert_eq!((ccmp.pn, ccmp.key_id), (17, 0)); + + let msdu = msdus(&d, plaintext).unwrap().next().unwrap().unwrap(); + let eth = Ethernet::from_msdu(msdu).unwrap(); + // From DS: Address 1 is the destination and Address 3 the source. + assert_eq!((eth.dst, eth.src), (d.addr1, d.addr3)); + assert_eq!(eth.ethertype, 0x0800); + assert_eq!(eth.payload[..2], [0x45, 0x00]); +} + +#[test] +fn a_captured_to_ds_frame_with_ht_control_reaches_ethernet_as_a_broadcast_dhcp_request() { + let frame = &frames("ieee802.11_htc")[0]; + let d = data(frame); + assert!(d.ht_control.is_some()); + let msdu = msdus(&d, d.body).unwrap().next().unwrap().unwrap(); + let eth = Ethernet::from_msdu(msdu).unwrap(); + // To DS: Address 3 is the destination and Address 2 the source. + assert_eq!((eth.dst, eth.src), (Address::BROADCAST, d.addr2)); + assert_eq!(eth.ethertype, 0x0800); + let udp = ð.payload[20..24]; + assert_eq!(udp, [0, 68, 0, 67]); +} diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap new file mode 100644 index 0000000000000000000000000000000000000000..613a4437da5106d23f552c84a87cf7606ef87342 GIT binary patch literal 4499 zcmc(je{54#6vxkbeYCahN{c{%4k8hSh)5m6aA8Tuex(1H%q;%FAD9JAke^dPf$+yF zSpo)Q^bd{TkfA_u5wc7`V}e#mBt$?$2FVg;Mo9bv9TE(Igl6^JdwaUu>u&S%9~cP1S~??|seJvgu6(2* zij}jq-i5|xjjUYxkZc>b*^%};Gy_`iIY>G79RZk6ZDr%Du%>3QBqTo=wi0ss)_&U9~8zBe&P+?6%MoS6N zf_fxmcmfI8eJg4uq-CUABjlVx2wNus&YT(JJXb(StCf&RB_Gfgs_zcw_w@=R_<{Q8Kkf_LN4D|VHF{CPZs7749GmH4)vfO35mVQ4*uUch2kVTKSh+_+ z9#B{tAxm>AVJ0DNDIr==kA$QqkdO`ij+KyIqk5jdGYDa85HK1Yd^=}V-Uhtx+D@QD)&gpT?%U>q$Z%knok+OP6+BjJrdGBL4+Ko zJkQw%u!~Y9nz96U12ta@ELSKEC zkTSHx{@AGfK$r5jQF69UMQC2$%u+m*T!fvn6W}e9A)2{3S-4MyH7A89^^$`gRC$rE zR975(Twk0dx6n%pT_6NpeHY*q?TcoXVDZQLxbGMxU~4sC(~HGdZskWh)4ReNsiXv1 zoa=@xH)^@NbSNNJKUBRd%j8{uib%TrDIT{cwx!b$p}rg#W8rST_=V8jr@sb z*6v`BUONJ<@KQKWuU$z{CvdprCG zg927DX%J)8DrTTk4hB~SMvrJ9>%U<9QjlJd9D@skGo#7W1V%4| S|IZ`=q=D=rh$wCnq8H+CvM>ToHV|QCVBlZFf!8(A8} zFA^{`EY{6UEzZ!*OD!qSFUl@tXJljmBF_Chn<8f#{{or^!UtFxX9IP=WZPmq4d{LI zQdS0Wp!pz-?)}`?_mI4Q3#0%H(*5tly?=}03Zny16yp62f6VmRT_9`+c955Ogd|i9 zfL_*>%9D@>c?sf|X;2z$UOhL60!lbAFevQjStNPD_}~9cj39hBlOf;}i+~pcqkw=F zP>BEs1Ct?#fIEYL$Af>xlX)YIri$$@u3gBnTp)x27<3RIV8g)3EWpG73}FFIFewBk z6&V;61e8I93WyK{5uOZe4CY!u)?LAX{8^k#Obm()Y^)3s5iEghMGShKhs#01)KbpM z@RET6jM0PX(_7?VLZp(nfrwzb0Zb(U3^ojq076bB3=F(;LV+e@O(g@DKDtwKrvm_Y CsJMv$ literal 0 HcmV?d00001 diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap new file mode 100644 index 0000000000000000000000000000000000000000..a4acaf6cc539ab94829fd2a9a4d1dbb11cabe476 GIT binary patch literal 295 zcmca|c+)~A1{MYw_zz@cGB6l`fV~qx1BlN6ly3lw5Fogb)^I literal 0 HcmV?d00001 diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap new file mode 100644 index 0000000000000000000000000000000000000000..b825a0a3f3bb67e6a76ea04c9be66b9d9c9fa236 GIT binary patch literal 541 zcmca|c+)~A1{MYw`2U}Qp&rP2JhM5_)0~B2J&+B;3=FCa+8!JXDKcOVg8;)k&IXpV zj0_Cws_YOU28IqN9fpj>N0{AiDb(}_*jqtKpdvvA1_h9+RjZhRBnN{l14GE3OAHJS zj8-)VRxq*wG0+Xxiv@TY_(4{K)OI(4)G{y#2nq>{h>D3zNJ>e|$jZqpC@LwdsH&-J zXliNe=<4Yk7#bOyn3|by+_wMV>m!R5jxEq>H{xjt{FKMUP!7}&!Z7FC@+*T~3vvD> zknW QrF7gqdB6Imavl|N0Oj9t;s5{u literal 0 HcmV?d00001 diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap new file mode 100644 index 0000000000000000000000000000000000000000..4634bc22997a4567b1b750490307825f5d840bf3 GIT binary patch literal 335 zcmca|c+)~A1{MYw$OAGm85j&ez}|_UAq*_R0uca)>; + +/// One element: often typed and well formed, sometimes bent, sometimes unknown. +fn draw_element(rng: &mut Rng) -> Vec { + let suite = |rng: &mut Rng| [0x00, 0x0F, 0xAC, rng.below(14) as u8]; + match rng.below(14) { + 0 => element(0, &rng.bytes(34)), + 1 => element(1, &rng.bytes(9)), + 2 => element(50, &rng.bytes(12)), + 3 => element(3, &rng.bytes(2)), + 4 => { + let mut body = vec![rng.below(4) as u8, rng.below(4) as u8, rng.next() as u8]; + body.extend(rng.bytes(8)); + element(5, &body) + } + 5 => { + let mut body = [b"US ", b"DE\x04", b"jp ", b"GB "][rng.below(4)].to_vec(); + for _ in 0..rng.below(5) { + body.extend([[1, 13, 20], [36, 4, 23], [190, 12, 30], [201, 81, 0], [0, 1, 1]][rng.below(5)]); + } + if rng.chance(30) { + body.push(rng.below(2) as u8); + } + element(7, &body) + } + 6 => { + let mut body = vec![1, 0]; + body.extend(suite(rng)); + for _ in 0..2 { + let n = rng.below(3); + body.extend((n as u16).to_le_bytes()); + (0..n).for_each(|_| body.extend(suite(rng))); + } + body.extend([[0x00, 0x00], [0xC0, 0x00], [0x80, 0x00], [0x40, 0x00]][rng.below(4)]); + body.truncate(rng.below(body.len() + 1).max(2)); + element(48, &body) + } + 7 => element(244, &[rng.below(4) as u8 | 0x20, rng.next() as u8][..rng.below(2) + 1]), + 8 => element(45, &rng.bytes(27)), + 9 => { + let mut body = rng.bytes(22); + body.resize(22, 0); + element(61, &body) + } + 10 => element(191, &rng.bytes(13)), + 11 => element(192, &[rng.below(5) as u8, 42, 0, 0xFC, 0xFF]), + 12 => element(127, &rng.bytes(10)), + _ => { + let id = rng.next() as u8; + if id == 255 { element(255, &rng.bytes(4)) } else { element(id, &rng.bytes(20)) } + } + } +} + +/// A management frame: Frame Control's subtype, a body of its fixed fields, and maybe elements. +fn draw_management(rng: &mut Rng) -> Vec { + let subtype = [8, 5, 4, 11, 12, 10, 0, 1, 2, 3, 13, 14][rng.below(12)]; + let mut f = common::management(subtype, &[]); + f.extend((0..12).map(|_| rng.next() as u8)); + if subtype == 8 || subtype == 5 { + f[32..34].copy_from_slice(&100u16.to_le_bytes()); + } + for _ in 0..rng.below(8) { + f.extend(draw_element(rng)); + } + f +} + +fn draw_block_ack(rng: &mut Rng) -> Vec { + let bar = rng.chance(50); + let mut f = vec![if bar { 0x84 } else { 0x94 }, 0, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend([if rng.chance(80) { 0x04 } else { rng.next() as u8 }, (rng.below(16) << 4) as u8]); + f.extend(((rng.below(4096) << 4) as u16).to_le_bytes()); + if !bar { + f.extend(rng.next().to_le_bytes()); + } + f +} + +/// A data frame from the access point and the Ethernet frames it was built from. +fn draw_data(rng: &mut Rng) -> (Vec, Sent) { + let ethertypes = [0x0800, 0x0806, 0x86DD, 0x888E, 0x8137, 0x80F3]; + let mut sent = Vec::new(); + let amsdu = rng.chance(40); + let protected = rng.chance(30); + let mut body = Vec::new(); + if protected { + let pn = rng.next() & 0xFFFF_FFFF_FFFF; + let p = pn.to_le_bytes(); + body.extend([p[0], p[1], 0, 0x20 | (rng.below(4) as u8) << 6, p[2], p[3], p[4], p[5]]); + } + let count = if amsdu { 1 + rng.below(4) } else { 1 }; + for i in 0..count { + let (da, sa) = if amsdu { (rng.address(), rng.address()) } else { (STA, common::PEER) }; + let ethertype = ethertypes[rng.below(ethertypes.len())]; + let payload = rng.bytes(60); + let msdu = [&llc_snap(ethertype).unwrap()[..], &payload].concat(); + if amsdu { + body.extend(common::subframe(da, sa, &msdu, i + 1 == count)); + } else { + body.extend(&msdu); + } + sent.push((da, sa, ethertype, payload)); + } + let mut f = common::qos_from_ds(amsdu, &body); + if protected { + f[1] |= 0x40; + } + (f, sent) +} + +/// Bend `frame` one way, or not at all; whether it changed. +fn mutate(rng: &mut Rng, frame: &mut Vec) -> bool { + if !rng.chance(40) || frame.is_empty() { + return false; + } + match rng.below(4) { + 0 => frame.truncate(rng.below(frame.len())), + 1 => { + let at = rng.below(frame.len()); + frame[at] ^= 1 + rng.below(255) as u8; + } + 2 => frame.extend(rng.bytes(8).into_iter().chain([rng.next() as u8])), + _ => { + let at = rng.below(frame.len()); + frame[at] = 0xFF; + } + } + true +} + +/// Read `frame` through every layer; the first refusal, or the Ethernet frames it carries. +fn read(frame: &[u8]) -> Result { + match Frame::parse(frame)? { + Frame::Management(m) => { + let elements = match Body::parse(m.subtype, m.body)? { + Body::Beacon(b) | Body::ProbeResponse(b) => b.elements, + Body::ProbeRequest { elements } => elements, + Body::AssociationRequest(a) | Body::ReassociationRequest(a) => a.elements, + Body::AssociationResponse(a) | Body::ReassociationResponse(a) => a.elements, + _ => return Ok(Vec::new()), + }; + let e = Elements::parse(elements)?; + if let Some(tim) = e.tim { + (0..=2008).for_each(|aid| _ = tim.buffered(aid)); + } + if let Some(country) = e.country { + country.triplets().for_each(drop); + } + Ok(Vec::new()) + } + Frame::BlockAck(ba) => { + (0..4096).for_each(|s| _ = ba.acked(Seq::new(s))); + Ok(Vec::new()) + } + Frame::BlockAckReq(_) => Ok(Vec::new()), + Frame::Data(d) => { + let plaintext = if d.flags.protected { CcmpHeader::split(d.body)?.1 } else { d.body }; + let mut out = Vec::new(); + for msdu in msdus(&d, plaintext)? { + let eth = Ethernet::from_msdu(msdu?)?; + out.push((eth.dst, eth.src, eth.ethertype, eth.payload.to_vec())); + } + Ok(out) + } + } +} + +#[test] +fn every_drawn_frame_is_read_or_refused_by_name_and_an_unbent_one_reads_back() { + let mut outcomes: BTreeMap = BTreeMap::new(); + let mut read_back = 0; + for iteration in 0..FUZZ_ITERATIONS { + let mut rng = Rng(iteration); + let (mut frame, sent) = match rng.below(3) { + 0 => (draw_management(&mut rng), None), + 1 => (draw_block_ack(&mut rng), None), + _ => { + let (f, sent) = draw_data(&mut rng); + (f, Some(sent)) + } + }; + let bent = mutate(&mut rng, &mut frame); + let result = read(&frame); + let key = match &result { + Ok(_) => "read".to_string(), + Err(e) => format!("{e:?}"), + }; + *outcomes.entry(key).or_default() += 1; + if let (Some(sent), false) = (sent, bent) { + assert_eq!(result.as_ref().ok(), Some(&sent), "iteration {iteration}: an unbent data frame"); + read_back += 1; + } + } + println!("{FUZZ_ITERATIONS} frames, {read_back} read back: {outcomes:#?}"); + assert!(read_back > FUZZ_ITERATIONS as usize / 5, "the fuzz reads data frames back"); + assert!(outcomes.len() > 30, "the fuzz reaches most refusals: {}", outcomes.len()); +} + +#[test] +fn the_reorder_window_passes_every_accepted_frame_up_once_and_in_order() { + for iteration in 0..FUZZ_ITERATIONS / 10 { + let mut rng = Rng(1 << 40 | iteration); + let size = 1 + rng.below(64) as u16; + let first = rng.below(4096) as u32; + let mut w = Window::new(Seq::new(first as u16), size).unwrap(); + let mut base = first; + let mut accepted = 0; + let mut out: Vec = Vec::new(); + for _ in 0..rng.below(300) { + if rng.chance(5) { + base += rng.below(size as usize * 2) as u32; + w.release_to(Seq::new(base as u16), &mut out); + continue; + } + let position = (base + rng.below(size as usize * 2) as u32).saturating_sub(rng.below(size as usize) as u32); + match w.receive(Seq::new(position as u16), position, &mut out) { + Ok(()) => accepted += 1, + Err(Refused::OldSequence | Refused::DuplicateSequence) => {} + Err(other) => panic!("iteration {iteration}: {other:?}"), + } + base = base.max(position.saturating_sub(size as u32 - 1)); + } + w.release_to(w.start().add(2047), &mut out); + assert!(out.windows(2).all(|p| p[0] < p[1]), "iteration {iteration}: out of order: {out:?}"); + assert_eq!(out.len(), accepted, "iteration {iteration}: a frame lost or passed up twice"); + } +} diff --git a/lib/ieee80211/tests/refusals.rs b/lib/ieee80211/tests/refusals.rs new file mode 100644 index 00000000000..2bfabc16045 --- /dev/null +++ b/lib/ieee80211/tests/refusals.rs @@ -0,0 +1,264 @@ +//! Every refusal, reached by a hostile input of its own, and the boundaries beside them accepted. + +mod common; + +use std::collections::BTreeSet; + +use common::{beacon, element, management, qos_from_ds, subframe, AP, PEER, STA}; +use toyos_ieee80211::data::{llc_snap, msdus, CcmpHeader, Ethernet, Msdu}; +use toyos_ieee80211::element::Elements; +use toyos_ieee80211::frame::{Frame, ManagementSubtype}; +use toyos_ieee80211::mgmt::Body; +use toyos_ieee80211::reorder::Window; +use toyos_ieee80211::{Address, Refused, Seq}; + +fn refusal(result: Result) -> Refused { + match result { + Ok(_) => panic!("accepted"), + Err(e) => e, + } +} + +fn frame(bytes: &[u8]) -> Refused { + refusal(Frame::parse(bytes)) +} + +fn elements(list: &[u8]) -> Refused { + refusal(Elements::parse(list)) +} + +fn country(body: &[u8]) -> Refused { + elements(&element(7, body)) +} + +/// A header of `fc` and 22 octets of zeros: two addresses' worth past Duration and the third. +fn header(fc: [u8; 2]) -> Vec { + let mut f = fc.to_vec(); + f.resize(24, 0); + f +} + +fn ethernet(payload: &[u8]) -> Refused { + refusal(Ethernet::from_msdu(Msdu { da: STA, sa: PEER, payload })) +} + +fn amsdu(body: &[u8]) -> Refused { + let f = qos_from_ds(true, body); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + match msdus(&d, d.body) { + Err(e) => e, + Ok(mut m) => refusal(m.try_fold((), |(), r| r.map(drop))), + } +} + +/// A BlockAckReq (subtype 8) or BlockAck (9) after its RA and TA. +fn block_ack(subtype: u8, rest: &[u8]) -> Refused { + let mut f = vec![subtype << 4 | 0x04, 0, 0, 0]; + f.extend(AP.0); + f.extend(STA.0); + f.extend(rest); + frame(&f) +} + +fn window() -> Window { + Window::new(Seq::new(100), 64).unwrap() +} + +/// One hostile input per refusal. +fn cases() -> Vec<(Refused, Refused)> { + use Refused::*; + let mut fragmented = management(8, &[]); + fragmented[22] = 0x01; + let mut group_sa = management(8, &[]); + group_sa[10] |= 1; + let rsn_v1 = |rest: &[u8]| element(48, &[&[1, 0][..], rest].concat()); + let ht_operation = |first: u8| element(61, &[&[6, first][..], &[0; 20]].concat()); + vec![ + (Truncated, frame(&[0x80, 0, 0])), + (ProtocolVersion, frame(&header([0x81, 0]))), + (ExtensionFrame, frame(&header([0x0C, 0]))), + (ReservedSubtype, frame(&header([0x70, 0]))), + (ManagementWithDs, frame(&header([0x80, 0x01]))), + (UnhandledManagement, frame(&header([0x90, 0]))), + (UnhandledControl, frame(&[0xD4, 0, 0, 0, 2, 0, 0, 0, 0, 1])), + (ContentionFreeData, frame(&header([0x18, 0]))), + (StrictlyOrdered, frame(&header([0x08, 0x80]))), + (Fragmented, frame(&fragmented)), + (GroupTransmitter, frame(&group_sa)), + (ZeroBeaconInterval, refusal(Body::parse(ManagementSubtype::Beacon, &[0; 12]))), + (AssociationId, refusal(Body::parse(ManagementSubtype::AssociationResponse, &[0, 0, 0, 0, 0, 0xC0]))), + (ElementPastBody, elements(&[0, 5, b'a'])), + (ExtensionWithoutId, elements(&[255, 0])), + (DuplicateElement, elements(&[element(0, b"a"), element(0, b"b")].concat())), + (ElementLength, elements(&element(3, &[1, 1]))), + (SsidLength, elements(&element(0, &[b'a'; 33]))), + (RateCount, elements(&element(1, &[2; 9]))), + (DtimPeriod, elements(&element(5, &[0, 0, 0, 0]))), + (TimBitmap, elements(&element(5, &[0, 1, 250, 0, 0]))), + (CountryString, country(b"us \x01\x0b\x14")), + (CountryPad, country(b"US \x01\x0b\x14\x01")), + (CountryTriplet, country(b"US \x01\x0b")), + (CountryChannels, country(b"US \x24\x00\x17")), + (RsnVersion, elements(&element(48, &[2, 0]))), + (RsnCount, elements(&rsn_v1(&[0x00, 0x0F, 0xAC, 4, 2, 0, 0x00, 0x0F, 0xAC, 4]))), + ( + MfpRequiredNotCapable, + elements(&rsn_v1(&[0, 0x0F, 0xAC, 4, 1, 0, 0, 0x0F, 0xAC, 4, 1, 0, 0, 0x0F, 0xAC, 8, 0x40, 0])), + ), + (ExtendedRsnLength, elements(&element(244, &[0x02, 0x20]))), + (SecondaryChannelOffset, elements(&ht_operation(2))), + (VhtReserved, elements(&element(192, &[4, 42, 0, 0xFF, 0xFF]))), + (BlockAckVariant, block_ack(8, &[0x00, 0x00, 0x00, 0x00])), + (BitmapSize, block_ack(9, &[[0x04, 0x00, 0x01, 0x00].as_slice(), &[0xFF; 8]].concat())), + (ControlLength, block_ack(8, &[0x04, 0x00, 0x00, 0x00, 0x00])), + (SubframeLength, amsdu(&[STA.0.as_slice(), &PEER.0, &[0, 100], &[0; 10]].concat())), + (SubframePadding, amsdu(&subframe(STA, PEER, b"x", false))), + (SnapHeaderAsAddress, amsdu(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x08, 0x00, 0x45, 0, 0, 0, 0, 0])), + (NotSnap, ethernet(&[0xF0, 0xF0, 0x03, 0, 0, 0, 0x08, 0x00])), + (UnknownOui, ethernet(&[0xAA, 0xAA, 0x03, 0x00, 0x10, 0x18, 0x08, 0x00])), + (TranslatedType, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x81, 0x37])), + (LengthField, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x05, 0xFF])), + (ExtendedIvClear, refusal(CcmpHeader::split(&[1, 0, 0, 0, 0, 0, 0, 0]))), + (WindowSize, refusal(Window::::new(Seq::new(0), 65))), + (OldSequence, refusal(window().receive(Seq::new(100 + 2048), 0, &mut Vec::new()))), + (DuplicateSequence, { + let mut w = window(); + w.receive(Seq::new(105), 0, &mut Vec::new()).unwrap(); + refusal(w.receive(Seq::new(105), 1, &mut Vec::new())) + }), + ] +} + +#[test] +fn every_refusal_has_a_hostile_input_that_produces_it() { + let cases = cases(); + for (wanted, got) in &cases { + assert_eq!(got, wanted); + } + let named: BTreeSet = cases.iter().map(|(wanted, _)| *wanted).collect(); + let all: BTreeSet = Refused::ALL.iter().copied().collect(); + assert_eq!(all.difference(&named).collect::>(), Vec::<&Refused>::new()); +} + +#[test] +fn every_prefix_of_a_frame_is_refused_as_truncated_short_of_its_header() { + let f = management(8, &beacon(&element(0, b"net"))); + for len in 0..24 { + assert_eq!(frame(&f[..len]), Refused::Truncated, "{len} octets"); + } + for len in 24..36 { + let Ok(Frame::Management(m)) = Frame::parse(&f[..len]) else { panic!("{len} octets") }; + assert_eq!(refusal(Body::parse(m.subtype, m.body)), Refused::Truncated, "{len} octets"); + } +} + +#[test] +fn an_element_whose_length_runs_one_past_the_body_is_refused() { + let mut list = element(0, b"net"); + list[1] = 4; + assert_eq!(elements(&list), Refused::ElementPastBody); + assert_eq!(elements(&[0]), Refused::ElementPastBody); +} + +#[test] +fn a_malformed_country_element_is_refused_whichever_rule_it_breaks() { + assert_eq!(country(b"US"), Refused::ElementLength); + assert_eq!(country(b"US "), Refused::ElementLength); + assert_eq!(country(b"US \x01"), Refused::CountryPad); + assert_eq!(country(b"U1 \x01\x0b\x14"), Refused::CountryString); + assert_eq!(country(b"US \x00\x0b\x14"), Refused::CountryChannels); + assert_eq!(country(b"US \xbe\x0c\x14"), Refused::CountryChannels); + assert_eq!(country(b"US \x01\x0b\x14\x24\x04"), Refused::CountryTriplet); +} + +#[test] +fn a_country_element_at_its_bounds_is_accepted() { + use toyos_ieee80211::element::Triplet; + // Channel 190 onward, eleven of them, ends at channel 200; a pad of 0 evens the length. + let list = element(7, b"DE \x01\x0d\x14\xbe\x0b\xf6\xc9\x51\x00\x00"); + let c = Elements::parse(&list).unwrap().country.unwrap(); + assert_eq!(&c.string, b"DE "); + assert_eq!( + c.triplets().collect::>(), + [ + Triplet::Subband { first_channel: 1, channels: 13, max_power_dbm: 20 }, + Triplet::Subband { first_channel: 190, channels: 11, max_power_dbm: -10 }, + Triplet::Operating { extension_id: 201, class: 81, coverage_class: 0 }, + ] + ); +} + +#[test] +fn an_ssid_of_32_octets_and_a_tim_reaching_aid_2007_are_accepted() { + let tim_last_octet = [&[0, 1, 250][..], &[0x80]].concat(); + let list = [element(0, &[b'a'; 32]), element(5, &tim_last_octet)].concat(); + let e = Elements::parse(&list).unwrap(); + assert_eq!(e.ssid.unwrap().len(), 32); + let tim = e.tim.unwrap(); + assert!(tim.buffered(2007)); + assert!(!tim.buffered(2006)); + assert!(!tim.buffered(7)); +} + +#[test] +fn an_association_response_takes_aid_1_to_2007_with_its_two_high_bits_set() { + let response = |aid: u16| { + let body = [&0u16.to_le_bytes()[..], &0u16.to_le_bytes(), &(aid | 0xC000).to_le_bytes()].concat(); + match Body::parse(ManagementSubtype::AssociationResponse, &body) { + Ok(Body::AssociationResponse(r)) => Ok(r.aid), + Ok(_) => panic!("another body"), + Err(e) => Err(e), + } + }; + assert_eq!(response(1), Ok(Some(1))); + assert_eq!(response(2007), Ok(Some(2007))); + assert_eq!(response(2008), Err(Refused::AssociationId)); +} + +#[test] +fn an_rsn_element_may_end_after_any_whole_field_and_not_inside_one() { + let full = [ + &[1, 0][..], + &[0x00, 0x0F, 0xAC, 4], + &[1, 0, 0x00, 0x0F, 0xAC, 4], + &[1, 0, 0x00, 0x0F, 0xAC, 8], + &[0xC0, 0x00], + &[0, 0], + &[0x00, 0x0F, 0xAC, 6], + ] + .concat(); + let field_ends = [2, 6, 12, 18, 20, 22, 26]; + for len in 2..=full.len() { + let list = element(48, &full[..len]); + let result = Elements::parse(&list); + assert_eq!(result.is_ok(), field_ends.contains(&len), "{len} octets: {:?}", result.err()); + } + let list = element(48, &full); + let rsn = Elements::parse(&list).unwrap().rsn.unwrap(); + assert!(rsn.mfp_required() && rsn.mfp_capable()); +} + +#[test] +fn llc_snap_refuses_a_length_and_tunnels_aarp_and_ipx() { + assert_eq!(llc_snap(0x05FF), Err(Refused::LengthField)); + assert_eq!(llc_snap(0x8137).unwrap()[..6], [0xAA, 0xAA, 0x03, 0x00, 0x00, 0xF8]); + assert_eq!(llc_snap(0x80F3).unwrap()[..6], [0xAA, 0xAA, 0x03, 0x00, 0x00, 0xF8]); + assert_eq!(llc_snap(0x0800).unwrap(), [0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00]); +} + +#[test] +fn a_bridge_tunnel_msdu_is_its_ethertype_on_ethernet() { + let eth = Ethernet::from_msdu(Msdu { da: STA, sa: PEER, payload: &[0xAA, 0xAA, 0x03, 0, 0, 0xF8, 0x81, 0x37, 9] }).unwrap(); + assert_eq!((eth.ethertype, eth.payload), (0x8137, &[9][..])); +} + +#[test] +fn a_block_ack_transmitter_with_its_bandwidth_signaling_bit_is_the_individual_address() { + let mut f = vec![0x84, 0, 0, 0]; + f.extend(STA.0); + f.extend([AP.0[0] | 1, AP.0[1], AP.0[2], AP.0[3], AP.0[4], AP.0[5]]); + f.extend([0x04, 0x30, 0x10, 0x00]); + let Ok(Frame::BlockAckReq(bar)) = Frame::parse(&f) else { panic!("not a BlockAckReq") }; + assert_eq!((bar.ta, bar.tid, bar.ssn), (AP, 3, Seq::new(1))); + assert_ne!(bar.ta, Address::BROADCAST); +} diff --git a/lib/ieee80211/tests/reorder.rs b/lib/ieee80211/tests/reorder.rs new file mode 100644 index 00000000000..db98c4b3edc --- /dev/null +++ b/lib/ieee80211/tests/reorder.rs @@ -0,0 +1,107 @@ +//! The receive reordering buffer against 10.25.6's rules, one rule per test. + +use toyos_ieee80211::reorder::Window; +use toyos_ieee80211::{Refused, Seq}; + +fn window(start: u16, size: u16) -> Window { + Window::new(Seq::new(start), size).unwrap() +} + +fn receive(w: &mut Window, seq: u16) -> Vec { + let mut out = Vec::new(); + w.receive(Seq::new(seq), seq, &mut out).unwrap(); + out +} + +#[test] +fn frames_in_order_pass_up_as_they_arrive() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 10), [10]); + assert_eq!(receive(&mut w, 11), [11]); + assert_eq!(w.start(), Seq::new(12)); +} + +#[test] +fn a_hole_holds_the_frames_after_it_until_it_fills() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 12), []); + assert_eq!(receive(&mut w, 11), []); + assert_eq!(w.first_held(), Some(Seq::new(11))); + assert_eq!(receive(&mut w, 10), [10, 11, 12]); + assert_eq!(w.first_held(), None); +} + +#[test] +fn a_frame_past_the_end_moves_the_window_to_end_at_it() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 12), []); + assert_eq!(receive(&mut w, 14), []); + // 20 ends the window at 20, so it starts at 13: 12 passes up, then the run 13 lacks. + assert_eq!(receive(&mut w, 20), [12]); + assert_eq!(w.start(), Seq::new(13)); + assert_eq!(receive(&mut w, 13), [13, 14]); +} + +#[test] +fn the_last_slot_of_the_window_moves_nothing() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 17), []); + assert_eq!(w.start(), Seq::new(10)); +} + +#[test] +fn a_frame_a_whole_window_past_passes_up_everything_held() { + let mut w = window(10, 4); + receive(&mut w, 11); + receive(&mut w, 13); + assert_eq!(receive(&mut w, 2000), [11, 13]); + assert_eq!(w.start(), Seq::new(1997)); +} + +#[test] +fn a_frame_half_the_sequence_space_behind_is_old() { + let mut w = window(10, 8); + let mut out = Vec::new(); + assert_eq!(w.receive(Seq::new(9), 9, &mut out), Err(Refused::OldSequence)); + assert_eq!(w.receive(Seq::new(10 + 2048), 0, &mut out), Err(Refused::OldSequence)); + assert_eq!(receive(&mut w, 10 + 2047), []); +} + +#[test] +fn a_release_passes_up_what_is_held_before_it_and_the_run_from_it() { + let mut w = window(10, 8); + receive(&mut w, 11); + receive(&mut w, 13); + receive(&mut w, 14); + let mut out = Vec::new(); + w.release_to(Seq::new(13), &mut out); + assert_eq!(out, [11, 13, 14]); + assert_eq!(w.start(), Seq::new(15)); +} + +#[test] +fn a_release_at_or_behind_the_start_changes_nothing() { + let mut w = window(10, 8); + receive(&mut w, 12); + let mut out = Vec::new(); + w.release_to(Seq::new(10), &mut out); + w.release_to(Seq::new(5), &mut out); + assert_eq!((out, w.start()), (vec![], Seq::new(10))); +} + +#[test] +fn the_window_runs_across_the_end_of_the_sequence_space() { + let mut w = window(4094, 8); + assert_eq!(receive(&mut w, 1), []); + assert_eq!(receive(&mut w, 4095), []); + assert_eq!(receive(&mut w, 0), []); + assert_eq!(receive(&mut w, 4094), [4094, 4095, 0, 1]); + assert_eq!(w.start(), Seq::new(2)); +} + +#[test] +fn a_window_of_one_to_sixty_four_is_made_and_no_other() { + assert!(Window::::new(Seq::new(0), 1).is_ok()); + assert!(Window::::new(Seq::new(0), 64).is_ok()); + assert_eq!(Window::::new(Seq::new(0), 0).err(), Some(Refused::WindowSize)); +} diff --git a/lib/ieee80211/tests/translate.rs b/lib/ieee80211/tests/translate.rs new file mode 100644 index 00000000000..cb50467dcd8 --- /dev/null +++ b/lib/ieee80211/tests/translate.rs @@ -0,0 +1,53 @@ +//! MSDUs out of data frames, and Ethernet frames into them and back. + +mod common; + +use common::{qos_from_ds, subframe, AP, PEER, STA}; +use toyos_ieee80211::data::{llc_snap, msdus, Ethernet, Msdu, ToDs}; +use toyos_ieee80211::frame::{Ds, Frame}; +use toyos_ieee80211::{Address, Seq}; + +#[test] +fn an_amsdu_yields_each_subframe_with_its_own_addresses() { + let other = Address([0x02, 9, 9, 9, 9, 9]); + let body = [subframe(STA, PEER, b"abc", false), subframe(STA, other, b"defgh", true)].concat(); + let f = qos_from_ds(true, &body); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + let got: Vec = msdus(&d, d.body).unwrap().map(Result::unwrap).collect(); + assert_eq!( + got, + [Msdu { da: STA, sa: PEER, payload: b"abc" }, Msdu { da: STA, sa: other, payload: b"defgh" }] + ); +} + +#[test] +fn a_frame_without_the_amsdu_bit_is_one_msdu_between_its_header_addresses() { + let f = qos_from_ds(false, b"payload"); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + let got: Vec = msdus(&d, d.body).unwrap().map(Result::unwrap).collect(); + // From DS: Address 1 is the destination, Address 3 the source. + assert_eq!(got, [Msdu { da: STA, sa: PEER, payload: b"payload" }]); +} + +#[test] +fn a_null_frame_carries_no_msdu() { + let mut f = qos_from_ds(false, &[]); + f[0] = 0xC8; + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + assert!(d.null); + assert_eq!(msdus(&d, d.body).unwrap().count(), 0); +} + +#[test] +fn an_ethernet_frame_sent_to_the_ap_reads_back_as_itself() { + let header = ToDs { bssid: AP, sa: STA, da: PEER, seq: Seq::new(4095), tid: 6, protected: false }; + let payload = b"\x45\x00 an ipv4 datagram"; + let f = [&header.header()[..], &llc_snap(0x0800).unwrap(), payload].concat(); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + assert_eq!((d.ds, d.ra(), d.seq, d.qos.unwrap().tid()), (Ds::ToDs, AP, Seq::new(4095), 6)); + let msdu = msdus(&d, d.body).unwrap().next().unwrap().unwrap(); + assert_eq!( + Ethernet::from_msdu(msdu).unwrap(), + Ethernet { dst: PEER, src: STA, ethertype: 0x0800, payload } + ); +} diff --git a/licenses/BSD-3-Clause-tcpdump.txt b/licenses/BSD-3-Clause-tcpdump.txt new file mode 100644 index 00000000000..a10474d54ae --- /dev/null +++ b/licenses/BSD-3-Clause-tcpdump.txt @@ -0,0 +1,19 @@ +License: BSD + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. The names of the authors may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. From b7495e7564f53e74319901f2e38d0269eed0cdc8 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 09:11:07 +0200 Subject: [PATCH 2/4] toyos-ieee80211: a clause number takes a section sign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The source gate reads a four-part number such as 9.4.2.24 as a public IPv4 address; every clause the crate cites now reads §9.4.2.24. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/ieee80211/src/control.rs | 4 ++-- lib/ieee80211/src/data.rs | 2 +- lib/ieee80211/src/element/country.rs | 2 +- lib/ieee80211/src/element/ht.rs | 6 +++--- lib/ieee80211/src/element/mod.rs | 10 +++++----- lib/ieee80211/src/element/rsn.rs | 6 +++--- lib/ieee80211/src/element/tim.rs | 2 +- lib/ieee80211/src/frame.rs | 6 +++--- lib/ieee80211/src/lib.rs | 14 +++++++------- lib/ieee80211/src/mgmt.rs | 6 +++--- lib/ieee80211/src/reorder.rs | 4 ++-- lib/ieee80211/tests/common/mod.rs | 2 +- lib/ieee80211/tests/reorder.rs | 2 +- 13 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/ieee80211/src/control.rs b/lib/ieee80211/src/control.rs index 0dd8021f439..afcbdb95b27 100644 --- a/lib/ieee80211/src/control.rs +++ b/lib/ieee80211/src/control.rs @@ -1,4 +1,4 @@ -//! BlockAckReq (9.3.1.7) and BlockAck (9.3.1.8) of the Compressed variant, the only one an HT or +//! BlockAckReq (§9.3.1.7) and BlockAck (§9.3.1.8) of the Compressed variant, the only one an HT or //! VHT station's immediate block-ack agreement uses; their bodies follow Frame Control and //! Duration. @@ -38,7 +38,7 @@ impl BlockAck { /// The receiver and transmitter, the control field's TID, and the Starting Sequence Control. fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, 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). + // 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]); diff --git a/lib/ieee80211/src/data.rs b/lib/ieee80211/src/data.rs index 5f54bb764bc..6dfc8f02514 100644 --- a/lib/ieee80211/src/data.rs +++ b/lib/ieee80211/src/data.rs @@ -1,4 +1,4 @@ -//! Data frame bodies: the CCMP header (12.5.3.2), A-MSDU (9.3.2.2), and translation between an +//! Data frame bodies: the CCMP header (§12.5.3.2), A-MSDU (§9.3.2.2), and translation between an //! MSDU and an Ethernet II frame by IEEE 802.1H: an RFC 1042 LLC/SNAP header, or the bridge-tunnel //! one for the two EtherTypes 802.1H sends that way. diff --git a/lib/ieee80211/src/element/country.rs b/lib/ieee80211/src/element/country.rs index 8948b27acb0..5a82f846a97 100644 --- a/lib/ieee80211/src/element/country.rs +++ b/lib/ieee80211/src/element/country.rs @@ -1,4 +1,4 @@ -//! The Country element (9.4.2.8): a country string, then triplets, then a pad octet of 0 where +//! The Country element (§9.4.2.8): a country string, then triplets, then a pad octet of 0 where //! one makes the length even. use crate::Refused; diff --git a/lib/ieee80211/src/element/ht.rs b/lib/ieee80211/src/element/ht.rs index bc11561e509..3af9d51fb0d 100644 --- a/lib/ieee80211/src/element/ht.rs +++ b/lib/ieee80211/src/element/ht.rs @@ -1,5 +1,5 @@ -//! HT Capabilities (9.4.2.55), HT Operation (9.4.2.56), VHT Capabilities (9.4.2.157) and VHT -//! Operation (9.4.2.158): fixed-length elements, refused at any other length or where a field +//! HT Capabilities (§9.4.2.55), HT Operation (§9.4.2.56), VHT Capabilities (§9.4.2.157) and VHT +//! Operation (§9.4.2.158): fixed-length elements, refused at any other length or where a field //! holds a value the standard reserves. use crate::read::Reader; @@ -35,7 +35,7 @@ impl HtCapabilities { } } -/// Secondary Channel Offset (9.4.2.56). +/// Secondary Channel Offset (§9.4.2.56). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SecondaryChannel { None, diff --git a/lib/ieee80211/src/element/mod.rs b/lib/ieee80211/src/element/mod.rs index bbb512a2359..4f7d3d75f0e 100644 --- a/lib/ieee80211/src/element/mod.rs +++ b/lib/ieee80211/src/element/mod.rs @@ -1,7 +1,7 @@ -//! Elements (9.4.2): the TLV walk of a body's element list, and the elements a station decides +//! Elements (§9.4.2): the TLV walk of a body's element list, and the elements a station decides //! by, each checked against its definition and allowed once. //! -//! An element this module does not type is skipped, as 10.28.9 has a receiver ignore what it +//! An element this module does not type is skipped, as §10.28.9 has a receiver ignore what it //! does not know; one it types that breaks its definition refuses the whole list. mod country; @@ -17,7 +17,7 @@ pub use tim::Tim; use crate::read::Reader; use crate::Refused; -/// An element's identifier: an Element ID, or 255 and its Element ID Extension (9.4.2.1). +/// An element's identifier: an Element ID, or 255 and its Element ID Extension (§9.4.2.1). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Id { Base(u8), @@ -84,7 +84,7 @@ impl<'a> Iter<'a> { } } -/// A rate in units of 500 kb/s, or a BSS membership selector (9.4.2.3). +/// A rate in units of 500 kb/s, or a BSS membership selector (§9.4.2.3). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Rate { /// In the BSSBasicRateSet, or for a selector, required of a member. @@ -129,7 +129,7 @@ pub struct Elements<'a> { pub extended_capabilities: Option>, } -/// The Extended Capabilities element: bit n of its field, absent past its length, is 0 (9.4.2.26). +/// The Extended Capabilities element: bit n of its field, absent past its length, is 0 (§9.4.2.26). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ExtendedCapabilities<'a>(pub &'a [u8]); diff --git a/lib/ieee80211/src/element/rsn.rs b/lib/ieee80211/src/element/rsn.rs index 42000ffbc22..680ee23d7a7 100644 --- a/lib/ieee80211/src/element/rsn.rs +++ b/lib/ieee80211/src/element/rsn.rs @@ -1,13 +1,13 @@ -//! The RSN element (9.4.2.24) and the RSN Extension element (9.4.2.241). +//! The RSN element (§9.4.2.24) and the RSN Extension element (§9.4.2.241). //! //! An RSNE's fields after its version are each optional, but only from the end: one present -//! needs every one before it. What follows the last field is ignored, as 10.28.9 has a receiver +//! needs every one before it. What follows the last field is ignored, as §10.28.9 has a receiver //! do with an extensible element. use crate::read::Reader; use crate::Refused; -/// A cipher or AKM suite selector: an OUI or CID and a type (9.4.2.24.2, 9.4.2.24.3). +/// A cipher or AKM suite selector: an OUI or CID and a type (§9.4.2.24.2, §9.4.2.24.3). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Suite(pub [u8; 4]); diff --git a/lib/ieee80211/src/element/tim.rs b/lib/ieee80211/src/element/tim.rs index 42b81de1ab3..b1f1cac3f17 100644 --- a/lib/ieee80211/src/element/tim.rs +++ b/lib/ieee80211/src/element/tim.rs @@ -1,4 +1,4 @@ -//! The TIM element (9.4.2.5). +//! The TIM element (§9.4.2.5). use crate::read::Reader; use crate::Refused; diff --git a/lib/ieee80211/src/frame.rs b/lib/ieee80211/src/frame.rs index 6de736db486..9b4f3486bae 100644 --- a/lib/ieee80211/src/frame.rs +++ b/lib/ieee80211/src/frame.rs @@ -1,4 +1,4 @@ -//! The MAC header (9.2.3, 9.2.4) of the frames a station handles: every management subtype it +//! The MAC header (§9.2.3, §9.2.4) of the frames a station handles: every management subtype it //! takes part in, BlockAckReq and BlockAck, and Data, Null, QoS Data and QoS Null. //! //! The input is one MPDU without its FCS. A body is handed back undecoded, because a protected @@ -17,7 +17,7 @@ pub enum Frame<'a> { Data(Data<'a>), } -/// The Frame Control flags a receiver acts on (9.2.4.1). +/// The Frame Control flags a receiver acts on (§9.2.4.1). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Flags { pub retry: bool, @@ -68,7 +68,7 @@ pub enum Ds { Both(Address), } -/// The QoS Control field (9.2.4.5). +/// The QoS Control field (§9.2.4.5). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct QosControl(pub u16); diff --git a/lib/ieee80211/src/lib.rs b/lib/ieee80211/src/lib.rs index 8239372a201..1adea47c549 100644 --- a/lib/ieee80211/src/lib.rs +++ b/lib/ieee80211/src/lib.rs @@ -71,7 +71,7 @@ macro_rules! refusals { refusals! { // The MAC header, 9.2. Truncated = Malformed, "the bytes end inside a field"; - ProtocolVersion = Unsupported, "the protocol version is not 0 (9.2.4.1.2)"; + ProtocolVersion = Unsupported, "the protocol version is not 0 (§9.2.4.1.2)"; ExtensionFrame = Unsupported, "a frame of type Extension (DMG and S1G only)"; ReservedSubtype = Malformed, "a subtype the standard reserves"; ManagementWithDs = Malformed, "a management frame with To DS or From DS set"; @@ -81,10 +81,10 @@ refusals! { StrictlyOrdered = Unsupported, "a non-QoS data frame with the Order bit set"; Fragmented = Unsupported, "a fragment of an MSDU or MMPDU"; GroupTransmitter = Malformed, "a transmitter address with the group bit set"; - // Management bodies, 9.3.3. + // Management bodies, §9.3.3. ZeroBeaconInterval = Malformed, "a beacon interval of 0 time units"; AssociationId = Malformed, "an association ID outside 1 to 2007 in a successful response"; - // Elements, 9.4.2. + // Elements, §9.4.2. ElementPastBody = Malformed, "an element's length runs past the frame body"; ExtensionWithoutId = Malformed, "an Element ID Extension element of length 0"; DuplicateElement = Malformed, "an element that may appear once appears twice"; @@ -103,11 +103,11 @@ refusals! { ExtendedRsnLength = Malformed, "an RSNXE whose field length runs past the element"; SecondaryChannelOffset = Malformed, "an HT Operation secondary channel offset of 2"; VhtReserved = Malformed, "a VHT capability or operation field of a reserved value"; - // Control frames, 9.3.1. + // Control frames, §9.3.1. BlockAckVariant = Unsupported, "a block-ack variant other than Compressed"; BitmapSize = Unsupported, "a compressed bitmap other than 64 bits"; ControlLength = Malformed, "a block-ack frame whose length its variant does not give"; - // Data, 9.3.2 and 12.5.3. + // Data, §9.3.2 and §12.5.3. SubframeLength = Malformed, "an A-MSDU subframe whose length runs past the MPDU"; SubframePadding = Malformed, "an A-MSDU subframe whose padding runs past the MPDU"; SnapHeaderAsAddress = Malformed, "an A-MSDU whose first destination is an LLC/SNAP header"; @@ -116,7 +116,7 @@ refusals! { TranslatedType = Malformed, "an RFC 1042 header carrying a type 802.1H sends by bridge-tunnel"; LengthField = Unsupported, "an EtherType below 0x0600, which is an IEEE 802.3 length"; ExtendedIvClear = Malformed, "a CCMP header without its Ext IV bit"; - // The reordering buffer, 10.25.6. + // The reordering buffer, §10.25.6. WindowSize = Malformed, "a reordering buffer of 0 or more than 64 MPDUs"; OldSequence = Malformed, "a sequence number before the window"; DuplicateSequence = Malformed, "a sequence number already held in the window"; @@ -142,7 +142,7 @@ impl Address { } } -/// A 12-bit sequence number, compared modulo 4096 (9.2.4.4.2, 10.3.2.14). +/// A 12-bit sequence number, compared modulo 4096 (§9.2.4.4.2, §10.3.2.14). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Seq(u16); diff --git a/lib/ieee80211/src/mgmt.rs b/lib/ieee80211/src/mgmt.rs index e095c1e9473..66898038c01 100644 --- a/lib/ieee80211/src/mgmt.rs +++ b/lib/ieee80211/src/mgmt.rs @@ -1,4 +1,4 @@ -//! Management frame bodies (9.3.3): the fixed fields of each subtype, and its elements still as +//! Management frame bodies (§9.3.3): the fixed fields of each subtype, and its elements still as //! bytes for [`crate::element::Elements::parse`]. //! //! An Authentication frame's fields past its status, an SAE exchange's among them, and an Action @@ -24,7 +24,7 @@ pub enum Body<'a> { ActionNoAck { category: u8, rest: &'a [u8] }, } -/// A Beacon or Probe Response (9.3.3.2, 9.3.3.10). +/// A Beacon or Probe Response (§9.3.3.2, §9.3.3.10). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Beacon<'a> { pub timestamp: u64, @@ -116,7 +116,7 @@ impl<'a> AssociationResponse<'a> { fn read(r: &mut Reader<'a>) -> Result { let capability = r.u16()?; let status = r.u16()?; - // The two most significant bits of the AID field are set (9.4.1.8). + // The two most significant bits of the AID field are set (§9.4.1.8). let aid = r.u16()? & 0x3FFF; let aid = match status { STATUS_SUCCESS if (1..=2007).contains(&aid) => Some(aid), diff --git a/lib/ieee80211/src/reorder.rs b/lib/ieee80211/src/reorder.rs index 2b4fd13d9e9..72b8f2bc22d 100644 --- a/lib/ieee80211/src/reorder.rs +++ b/lib/ieee80211/src/reorder.rs @@ -1,4 +1,4 @@ -//! The receive reordering buffer of one block-ack agreement (10.25.6): MPDUs held by sequence +//! The receive reordering buffer of one block-ack agreement (§10.25.6): MPDUs held by sequence //! number and passed up in order. //! //! A frame in the window is held, and the run from the window's start passes up. A frame past @@ -12,7 +12,7 @@ use core::num::{NonZeroU16, NonZeroUsize}; use crate::{Refused, Seq}; -/// The largest buffer an 802.11-2020 HT or VHT agreement negotiates (9.4.1.14). +/// The largest buffer an 802.11-2020 HT or VHT agreement negotiates (§9.4.1.14). pub const MAX_SIZE: u16 = 64; const HALF: u16 = Seq::MODULUS / 2; diff --git a/lib/ieee80211/tests/common/mod.rs b/lib/ieee80211/tests/common/mod.rs index dcef25bab37..5af704c4f5f 100644 --- a/lib/ieee80211/tests/common/mod.rs +++ b/lib/ieee80211/tests/common/mod.rs @@ -137,7 +137,7 @@ pub fn mpdu(record: &Record) -> Vec { } } -/// CRC-32 of IEEE 802.3 §3.2.9, which 802.11's FCS is (9.2.4.8). +/// CRC-32 of IEEE 802.3 §3.2.9, which 802.11's FCS is (§9.2.4.8). pub fn crc32(bytes: &[u8]) -> u32 { let mut crc = !0u32; for &b in bytes { diff --git a/lib/ieee80211/tests/reorder.rs b/lib/ieee80211/tests/reorder.rs index db98c4b3edc..8ed54ba968f 100644 --- a/lib/ieee80211/tests/reorder.rs +++ b/lib/ieee80211/tests/reorder.rs @@ -1,4 +1,4 @@ -//! The receive reordering buffer against 10.25.6's rules, one rule per test. +//! The receive reordering buffer against §10.25.6's rules, one rule per test. use toyos_ieee80211::reorder::Window; use toyos_ieee80211::{Refused, Seq}; From da6351833ee430e6bc92fb2345d993cd105098b3 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 11:28:40 +0200 Subject: [PATCH 3/4] toyos-ieee80211: HE elements, HE BlockAck bitmaps, a 256-frame window and WMM The orchestrator's rulings on #924 add IEEE 802.11ax-2021 and the Wi-Fi Alliance's WMM Specification as sources, so W1 carries the whole of its row. - HE Capabilities (extension 35): its length follows from the Channel Width Set's 160 and 80+80 bits and from the PPE Thresholds' own NSTS and RU mask (Refused::PpeThresholds); any other length is refused. - HE Operation (extension 36): the VHT, co-hosted BSS and 6 GHz fields present as its parameters say. - A Compressed BlockAck's bitmap is 32, 64, 128 or 256 bits as its Fragment Number's bits 2 and 1 say; a fragment level (bit 0) or bit 3 is Refused::BitmapSize. - The reorder window takes 1 to 256 MPDUs. - WMM Information and Parameter elements under 00-50-F2 type 2, version 1; another version, an AIFSN below 2 and a Parameter element that does not give each access category once are refused by name. Another WMM subtype and every other vendor element are skipped. issues/a-fragmented-wifi-frame-is-dropped.md records what refusing fragments costs and the reassembly that ends it. The track's writer line says root CLAUDE.md's clean-room rule is the sentence it asked for. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- issues/a-fragmented-wifi-frame-is-dropped.md | 32 +++++++ issues/there-is-no-wifi.md | 4 +- lib/ieee80211/src/control.rs | 65 +++++++++---- lib/ieee80211/src/element/he.rs | 99 ++++++++++++++++++++ lib/ieee80211/src/element/mod.rs | 26 ++++- lib/ieee80211/src/element/wmm.rs | 80 ++++++++++++++++ lib/ieee80211/src/lib.rs | 11 ++- lib/ieee80211/src/reorder.rs | 4 +- lib/ieee80211/tests/corpus.rs | 15 +++ lib/ieee80211/tests/fuzz.rs | 50 ++++++++-- lib/ieee80211/tests/refusals.rs | 83 +++++++++++++++- lib/ieee80211/tests/reorder.rs | 17 +++- 12 files changed, 447 insertions(+), 39 deletions(-) create mode 100644 issues/a-fragmented-wifi-frame-is-dropped.md create mode 100644 lib/ieee80211/src/element/he.rs create mode 100644 lib/ieee80211/src/element/wmm.rs diff --git a/issues/a-fragmented-wifi-frame-is-dropped.md b/issues/a-fragmented-wifi-frame-is-dropped.md new file mode 100644 index 00000000000..383c04aa742 --- /dev/null +++ b/issues/a-fragmented-wifi-frame-is-dropped.md @@ -0,0 +1,32 @@ +--- +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 drops part of the station's traffic, retry by retry, +with nothing upstream told why. + +- **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. diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 69ede7aa4a2..27f0fb8b2bf 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -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 diff --git a/lib/ieee80211/src/control.rs b/lib/ieee80211/src/control.rs index afcbdb95b27..5351164df6a 100644 --- a/lib/ieee80211/src/control.rs +++ b/lib/ieee80211/src/control.rs @@ -1,6 +1,10 @@ -//! BlockAckReq (§9.3.1.7) and BlockAck (§9.3.1.8) of the Compressed variant, the only one an HT or -//! VHT station's immediate block-ack agreement uses; their bodies follow Frame Control and -//! Duration. +//! 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}; @@ -23,20 +27,26 @@ pub struct BlockAck { pub ta: Address, pub tid: u8, pub ssn: Seq, - /// Bit n acknowledges sequence number SSN + n. - pub bitmap: u64, + /// 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 const fn acked(&self, seq: Seq) -> bool { + pub fn acked(&self, seq: Seq) -> bool { let offset = seq.since(self.ssn); - offset < 64 && self.bitmap >> offset & 1 != 0 + 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. -fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, u8, Seq), Refused> { +/// 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()?; @@ -47,19 +57,18 @@ fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, u8, Seq), Refused return Err(Refused::BlockAckVariant); } let [_, high] = control.to_le_bytes(); - let ssc = r.u16()?; - // A nonzero Fragment Number in a Compressed BlockAck sizes a bitmap past 64 bits, which the - // HE amendment defines and 802.11-2020 does not. - if ssc & 0x000F != 0 { - return Err(Refused::BitmapSize); - } - Ok((ra, ta, high >> 4, Seq::new(ssc >> 4))) + 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 { let mut r = Reader::new(after_duration); - let (ra, ta, tid, ssn) = compressed(&mut r)?; + let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?; + if fragment != 0 { + return Err(Refused::BitmapSize); + } if !r.is_empty() { return Err(Refused::ControlLength); } @@ -70,11 +79,27 @@ impl BlockAckReq { impl BlockAck { pub(crate) fn parse(after_duration: &[u8]) -> Result { let mut r = Reader::new(after_duration); - let (ra, ta, tid, ssn) = compressed(&mut r)?; - let bitmap = r.u64()?; + 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 }) + Ok(Self { ra, ta, tid, ssn, bitmap, bits }) } } diff --git a/lib/ieee80211/src/element/he.rs b/lib/ieee80211/src/element/he.rs new file mode 100644 index 00000000000..9cee877cea5 --- /dev/null +++ b/lib/ieee80211/src/element/he.rs @@ -0,0 +1,99 @@ +//! HE Capabilities (802.11ax-2021 §9.4.2.248) and HE Operation (§9.4.2.249), Element ID Extension +//! 35 and 36. Their lengths follow from bits inside them, and any other length is refused. + +use crate::read::Reader; +use crate::Refused; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HeCapabilities<'a> { + pub mac: [u8; 6], + pub phy: [u8; 11], + /// Rx and Tx HE-MCS maps for 80 MHz and narrower. + pub mcs_80: [u8; 4], + /// For 160 MHz, present when the Channel Width Set's bit 3 is. + pub mcs_160: Option<[u8; 4]>, + /// For 80+80 MHz, present when the Channel Width Set's bit 4 is. + pub mcs_80p80: Option<[u8; 4]>, + /// Present when the PHY capabilities' bit 23 is. + pub ppe_thresholds: Option<&'a [u8]>, +} + +impl<'a> HeCapabilities<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let mut r = Reader::new(body); + let length = |_| Refused::ElementLength; + let mac = r.array().map_err(length)?; + let phy: [u8; 11] = r.array().map_err(length)?; + let [width_set, _, ppe_octet, ..] = phy; + let mcs_80 = r.array().map_err(length)?; + let mcs_160 = if width_set & 1 << 3 != 0 { Some(r.array().map_err(length)?) } else { None }; + let mcs_80p80 = if width_set & 1 << 4 != 0 { Some(r.array().map_err(length)?) } else { None }; + let ppe_thresholds = if ppe_octet & 1 << 7 != 0 { Some(ppe(&mut r)?) } else { None }; + if !r.is_empty() { + return Err(Refused::ElementLength); + } + Ok(Self { mac, phy, mcs_80, mcs_160, mcs_80p80, ppe_thresholds }) + } +} + +/// The PPE Thresholds field: NSTS and an RU index bitmask, then a pair of 3-bit thresholds per +/// stream and RU size, padded to whole octets (§9.4.2.248.5). +fn ppe<'a>(r: &mut Reader<'a>) -> Result<&'a [u8], Refused> { + let &first = r.rest().first().ok_or(Refused::PpeThresholds)?; + let streams = u32::from(first & 0b111).saturating_add(1); + let sizes = (first >> 3 & 0x0F).count_ones(); + let bits = streams.saturating_mul(sizes).saturating_mul(6).saturating_add(7); + let octets = usize::try_from(bits.div_ceil(8)).map_err(|_| Refused::PpeThresholds)?; + r.take(octets).map_err(|_| Refused::PpeThresholds) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HeOperation { + /// The HE Operation Parameters field, 24 bits. + pub parameters: u32, + /// BSS Color Information: the color in bits 0 to 5, Partial bit 6, Disabled bit 7. + pub bss_color: u8, + pub basic_mcs: u16, + /// Channel Width and two center frequency segments, as VHT Operation's. + pub vht: Option<[u8; 3]>, + pub max_cohosted_bssid: Option, + pub six_ghz: Option, +} + +/// The 6 GHz Operation Information field. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SixGhz { + pub primary_channel: u8, + /// Channel Width in bits 0 and 1, Duplicate Beacon bit 2, Regulatory Info bits 3 to 5. + pub control: u8, + pub center_segment0: u8, + pub center_segment1: u8, + pub minimum_rate: u8, +} + +impl HeOperation { + const VHT_PRESENT: u32 = 1 << 14; + const COHOSTED: u32 = 1 << 15; + const SIX_GHZ_PRESENT: u32 = 1 << 17; + + pub(super) fn parse(body: &[u8]) -> Result { + let mut r = Reader::new(body); + let length = |_| Refused::ElementLength; + let [p0, p1, p2] = r.array().map_err(length)?; + let parameters = u32::from_le_bytes([p0, p1, p2, 0]); + let bss_color = r.u8().map_err(length)?; + let basic_mcs = r.u16().map_err(length)?; + let vht = if parameters & Self::VHT_PRESENT != 0 { Some(r.array().map_err(length)?) } else { None }; + let max_cohosted_bssid = if parameters & Self::COHOSTED != 0 { Some(r.u8().map_err(length)?) } else { None }; + let six_ghz = if parameters & Self::SIX_GHZ_PRESENT != 0 { + let [primary_channel, control, center_segment0, center_segment1, minimum_rate] = r.array().map_err(length)?; + Some(SixGhz { primary_channel, control, center_segment0, center_segment1, minimum_rate }) + } else { + None + }; + if !r.is_empty() { + return Err(Refused::ElementLength); + } + Ok(Self { parameters, bss_color, basic_mcs, vht, max_cohosted_bssid, six_ghz }) + } +} diff --git a/lib/ieee80211/src/element/mod.rs b/lib/ieee80211/src/element/mod.rs index 4f7d3d75f0e..483bcf62cbc 100644 --- a/lib/ieee80211/src/element/mod.rs +++ b/lib/ieee80211/src/element/mod.rs @@ -1,18 +1,23 @@ //! Elements (§9.4.2): the TLV walk of a body's element list, and the elements a station decides //! by, each checked against its definition and allowed once. //! -//! An element this module does not type is skipped, as §10.28.9 has a receiver ignore what it -//! does not know; one it types that breaks its definition refuses the whole list. +//! A vendor-specific element other than WMM's, and any other element this module does not type, is +//! skipped, as §10.28.9 has a receiver ignore what it does not know; one it types that breaks its +//! definition refuses the whole list. mod country; +mod he; mod ht; mod rsn; mod tim; +mod wmm; pub use country::{Country, Triplet, Triplets}; +pub use he::{HeCapabilities, HeOperation, SixGhz}; pub use ht::{HtCapabilities, HtOperation, SecondaryChannel, VhtCapabilities, VhtChannelWidth, VhtOperation}; pub use rsn::{Rsn, Rsnx, Suite, Suites}; pub use tim::Tim; +pub use wmm::{AcParameters, Wmm}; use crate::read::Reader; use crate::Refused; @@ -37,7 +42,10 @@ impl Id { pub const EXTENDED_CAPABILITIES: Self = Self::Base(127); pub const VHT_CAPABILITIES: Self = Self::Base(191); pub const VHT_OPERATION: Self = Self::Base(192); + pub const VENDOR_SPECIFIC: Self = Self::Base(221); pub const RSNX: Self = Self::Base(244); + pub const HE_CAPABILITIES: Self = Self::Extension(35); + pub const HE_OPERATION: Self = Self::Extension(36); const EXTENSION: u8 = 255; } @@ -97,6 +105,8 @@ impl Rate { pub const HT_PHY: u8 = 127; pub const VHT_PHY: u8 = 126; pub const SAE_H2E_ONLY: u8 = 123; + /// 802.11ax-2021 Table 9-78. + pub const HE_PHY: u8 = 122; } /// The Supported Rates and BSS Membership Selectors element and its Extended continuation. @@ -127,6 +137,9 @@ pub struct Elements<'a> { pub vht_capabilities: Option, pub vht_operation: Option, pub extended_capabilities: Option>, + pub he_capabilities: Option>, + pub he_operation: Option, + pub wmm: Option, } /// The Extended Capabilities element: bit n of its field, absent past its length, is 0 (§9.4.2.26). @@ -174,6 +187,15 @@ impl<'a> Elements<'a> { Id::VHT_CAPABILITIES => once(&mut e.vht_capabilities, VhtCapabilities::parse(body)?)?, Id::VHT_OPERATION => once(&mut e.vht_operation, VhtOperation::parse(body)?)?, Id::EXTENDED_CAPABILITIES => once(&mut e.extended_capabilities, ExtendedCapabilities(body))?, + Id::HE_CAPABILITIES => once(&mut e.he_capabilities, HeCapabilities::parse(body)?)?, + Id::HE_OPERATION => once(&mut e.he_operation, HeOperation::parse(body)?)?, + Id::VENDOR_SPECIFIC => { + if let Some(rest) = body.strip_prefix(&wmm::HEADER) + && let Some(w) = Wmm::parse(rest)? + { + once(&mut e.wmm, w)?; + } + } Id::Base(_) | Id::Extension(_) => {} } } diff --git a/lib/ieee80211/src/element/wmm.rs b/lib/ieee80211/src/element/wmm.rs new file mode 100644 index 00000000000..4ffbd3929e2 --- /dev/null +++ b/lib/ieee80211/src/element/wmm.rs @@ -0,0 +1,80 @@ +//! The WMM Information and Parameter elements (Wi-Fi Alliance, WMM Specification v1.2, §2.2.1 and +//! §2.2.2): vendor-specific elements under OUI 00-50-F2, OUI type 2, version 1. + +use crate::read::Reader; +use crate::Refused; + +/// The OUI and OUI type a WMM element's body starts with. +pub(super) const HEADER: [u8; 4] = [0x00, 0x50, 0xF2, 0x02]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Wmm { + Information { qos_info: u8 }, + Parameter { qos_info: u8, access_categories: [AcParameters; 4] }, +} + +/// One AC Parameter Record, indexed in [`Wmm::Parameter`] by its ACI: best effort, background, +/// video, voice. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AcParameters { + /// 2 to 15 slots. + pub aifsn: u8, + pub admission_control: bool, + /// The exponents of CWmin and CWmax: CW is 2^ECW − 1. + pub ecw_min: u8, + pub ecw_max: u8, + /// In units of 32 µs. + pub txop_limit: u16, +} + +const INFORMATION: u8 = 0; +const PARAMETER: u8 = 1; + +impl Wmm { + /// The element after its OUI and OUI type, or `None` for another WMM subtype (a TSPEC). + pub(super) fn parse(after_header: &[u8]) -> Result, Refused> { + let mut r = Reader::new(after_header); + let length = |_| Refused::ElementLength; + let subtype = r.u8().map_err(length)?; + if subtype != INFORMATION && subtype != PARAMETER { + return Ok(None); + } + if r.u8().map_err(length)? != 1 { + return Err(Refused::WmmVersion); + } + let qos_info = r.u8().map_err(length)?; + let wmm = if subtype == INFORMATION { + Self::Information { qos_info } + } else { + r.u8().map_err(length)?; + let mut access_categories = [None; 4]; + for _ in 0..4 { + let [aci_aifsn, ecw] = r.array().map_err(length)?; + let txop_limit = r.u16().map_err(length)?; + let aifsn = aci_aifsn & 0x0F; + if aifsn < 2 { + return Err(Refused::WmmAifsn); + } + let record = AcParameters { + aifsn, + admission_control: aci_aifsn & 1 << 4 != 0, + ecw_min: ecw & 0x0F, + ecw_max: ecw >> 4, + txop_limit, + }; + if let Some(slot) = access_categories.get_mut(usize::from(aci_aifsn >> 5 & 0b11)) { + *slot = Some(record); + } + } + // Four records fill all four categories only if no ACI repeats. + let [Some(be), Some(bk), Some(vi), Some(vo)] = access_categories else { + return Err(Refused::WmmAccessCategory); + }; + Self::Parameter { qos_info, access_categories: [be, bk, vi, vo] } + }; + if !r.is_empty() { + return Err(Refused::ElementLength); + } + Ok(Some(wmm)) + } +} diff --git a/lib/ieee80211/src/lib.rs b/lib/ieee80211/src/lib.rs index 1adea47c549..97b8f31cc2a 100644 --- a/lib/ieee80211/src/lib.rs +++ b/lib/ieee80211/src/lib.rs @@ -1,4 +1,5 @@ -//! IEEE Std 802.11-2020 frames as a station meets them: parsed in place from bytes a radio or a +//! IEEE Std 802.11-2020 frames as a station meets them, with 802.11ax-2021's HE elements and +//! BlockAck bitmaps and the Wi-Fi Alliance's WMM element: parsed in place from bytes a radio or a //! peer wrote, and refused by the first rule they break, by name. //! //! [`frame::Frame::parse`] takes one MPDU without its FCS and yields a typed management, control @@ -103,9 +104,13 @@ refusals! { ExtendedRsnLength = Malformed, "an RSNXE whose field length runs past the element"; SecondaryChannelOffset = Malformed, "an HT Operation secondary channel offset of 2"; VhtReserved = Malformed, "a VHT capability or operation field of a reserved value"; + PpeThresholds = Malformed, "HE PPE Thresholds whose length its own NSTS and RU mask do not give"; + WmmVersion = Unsupported, "a WMM element of a version other than 1"; + WmmAifsn = Malformed, "a WMM AC Parameter Record with an AIFSN below 2"; + WmmAccessCategory = Malformed, "a WMM Parameter element that does not give each access category once"; // Control frames, §9.3.1. BlockAckVariant = Unsupported, "a block-ack variant other than Compressed"; - BitmapSize = Unsupported, "a compressed bitmap other than 64 bits"; + BitmapSize = Unsupported, "a Fragment Number giving a fragment level or a bitmap other than 32, 64, 128 or 256 bits"; ControlLength = Malformed, "a block-ack frame whose length its variant does not give"; // Data, §9.3.2 and §12.5.3. SubframeLength = Malformed, "an A-MSDU subframe whose length runs past the MPDU"; @@ -117,7 +122,7 @@ refusals! { LengthField = Unsupported, "an EtherType below 0x0600, which is an IEEE 802.3 length"; ExtendedIvClear = Malformed, "a CCMP header without its Ext IV bit"; // The reordering buffer, §10.25.6. - WindowSize = Malformed, "a reordering buffer of 0 or more than 64 MPDUs"; + WindowSize = Malformed, "a reordering buffer of 0 or more than 256 MPDUs"; OldSequence = Malformed, "a sequence number before the window"; DuplicateSequence = Malformed, "a sequence number already held in the window"; } diff --git a/lib/ieee80211/src/reorder.rs b/lib/ieee80211/src/reorder.rs index 72b8f2bc22d..495720ed448 100644 --- a/lib/ieee80211/src/reorder.rs +++ b/lib/ieee80211/src/reorder.rs @@ -12,8 +12,8 @@ use core::num::{NonZeroU16, NonZeroUsize}; use crate::{Refused, Seq}; -/// The largest buffer an 802.11-2020 HT or VHT agreement negotiates (§9.4.1.14). -pub const MAX_SIZE: u16 = 64; +/// The largest buffer an HE agreement negotiates (802.11ax-2021 §9.4.1.14); HT and VHT stop at 64. +pub const MAX_SIZE: u16 = 256; const HALF: u16 = Seq::MODULUS / 2; pub struct Window { diff --git a/lib/ieee80211/tests/corpus.rs b/lib/ieee80211/tests/corpus.rs index a1f608b90fe..940544cc63a 100644 --- a/lib/ieee80211/tests/corpus.rs +++ b/lib/ieee80211/tests/corpus.rs @@ -102,6 +102,21 @@ fn a_captured_sae_beacon_reads_its_rsn_ht_and_vht_operation() { assert_eq!((tim.dtim_count, tim.dtim_period, tim.group_buffered()), (1, 2, false)); } +#[test] +fn a_captured_association_response_carries_the_wmm_default_parameters() { + use toyos_ieee80211::element::{AcParameters, Wmm}; + let response = &frames("ieee802.11_exthdr")[23]; + let Ok(Frame::Management(m)) = Frame::parse(response) else { panic!("not management") }; + let Ok(Body::AssociationResponse(r)) = Body::parse(m.subtype, m.body) else { panic!("not a response") }; + assert_eq!(r.aid, Some(1)); + let Some(Wmm::Parameter { access_categories: [be, bk, vi, vo], .. }) = Elements::parse(r.elements).unwrap().wmm else { + panic!("no WMM Parameter element") + }; + let ac = |aifsn, ecw_min, ecw_max, txop_limit| AcParameters { aifsn, admission_control: false, ecw_min, ecw_max, txop_limit }; + // AC_VI's TXOP is 3.008 ms and AC_VO's 1.504 ms: 94 and 47 units of 32 µs. + assert_eq!([be, bk, vi, vo], [ac(3, 4, 10, 0), ac(7, 4, 10, 0), ac(2, 3, 4, 94), ac(2, 2, 3, 47)]); +} + #[test] fn a_captured_ccmp_frame_keeps_its_header_and_carries_an_ipv4_msdu() { let frame = &frames("ieee802.11_rx-stbc")[0]; diff --git a/lib/ieee80211/tests/fuzz.rs b/lib/ieee80211/tests/fuzz.rs index fde19509ce9..190382bcf58 100644 --- a/lib/ieee80211/tests/fuzz.rs +++ b/lib/ieee80211/tests/fuzz.rs @@ -28,7 +28,7 @@ type Sent = Vec<(Address, Address, u16, Vec)>; /// One element: often typed and well formed, sometimes bent, sometimes unknown. fn draw_element(rng: &mut Rng) -> Vec { let suite = |rng: &mut Rng| [0x00, 0x0F, 0xAC, rng.below(14) as u8]; - match rng.below(14) { + match rng.below(18) { 0 => element(0, &rng.bytes(34)), 1 => element(1, &rng.bytes(9)), 2 => element(50, &rng.bytes(12)), @@ -70,6 +70,43 @@ fn draw_element(rng: &mut Rng) -> Vec { 10 => element(191, &rng.bytes(13)), 11 => element(192, &[rng.below(5) as u8, 42, 0, 0xFC, 0xFF]), 12 => element(127, &rng.bytes(10)), + 13 => { + let width_set = [0, 0x08, 0x18, 0x10][rng.below(4)]; + let ppe = rng.chance(40); + let mut body = vec![35]; + body.extend((0..6).map(|_| rng.next() as u8)); + body.extend([width_set, 0, if ppe { 0x80 } else { 0 }, 0, 0, 0, 0, 0, 0, 0, 0]); + body.extend(vec![0xFA; 4 + 4 * (width_set.count_ones() as usize)]); + if ppe { + let first = rng.next() as u8 & 0x7F; + let bits = 7 + 6 * ((first & 7) as usize + 1) * (first >> 3).count_ones() as usize; + body.push(first); + body.extend(vec![0; bits.div_ceil(8) - 1]); + } + element(255, &body) + } + 14 => { + let present = [0u32, 1 << 14, 1 << 15, 1 << 17, 1 << 14 | 1 << 15 | 1 << 17][rng.below(5)]; + let p = present.to_le_bytes(); + let mut body = vec![36, p[0], p[1], p[2], rng.next() as u8, 0xFC, 0xFF]; + let extra = 3 * usize::from(present & 1 << 14 != 0) + usize::from(present & 1 << 15 != 0) + 5 * usize::from(present & 1 << 17 != 0); + body.extend((0..extra).map(|_| rng.next() as u8)); + element(255, &body) + } + 15 | 16 => { + let mut body = vec![0x00, 0x50, 0xF2, 0x02, rng.below(3) as u8, 1, rng.next() as u8]; + if body[4] == 1 { + body.push(0); + let mut acis = [0u8, 1, 2, 3]; + if rng.chance(10) { + acis[3] = acis[rng.below(3)]; + } + for aci in acis { + body.extend([aci << 5 | (2 + rng.below(14)) as u8, rng.next() as u8, rng.next() as u8, 0]); + } + } + element(221, &body) + } _ => { let id = rng.next() as u8; if id == 255 { element(255, &rng.bytes(4)) } else { element(id, &rng.bytes(20)) } @@ -97,10 +134,9 @@ fn draw_block_ack(rng: &mut Rng) -> Vec { f.extend(STA.0); f.extend(AP.0); f.extend([if rng.chance(80) { 0x04 } else { rng.next() as u8 }, (rng.below(16) << 4) as u8]); - f.extend(((rng.below(4096) << 4) as u16).to_le_bytes()); - if !bar { - f.extend(rng.next().to_le_bytes()); - } + let (fragment, octets) = if bar { (0, 0) } else { [(0, 8), (2, 32), (4, 16), (6, 4)][rng.below(4)] }; + f.extend(((rng.below(4096) << 4) as u16 | fragment).to_le_bytes()); + f.extend((0..octets).map(|_| rng.next() as u8)); f } @@ -221,14 +257,14 @@ fn every_drawn_frame_is_read_or_refused_by_name_and_an_unbent_one_reads_back() { } println!("{FUZZ_ITERATIONS} frames, {read_back} read back: {outcomes:#?}"); assert!(read_back > FUZZ_ITERATIONS as usize / 5, "the fuzz reads data frames back"); - assert!(outcomes.len() > 30, "the fuzz reaches most refusals: {}", outcomes.len()); + assert!(outcomes.len() > 32, "the fuzz reaches most refusals: {}", outcomes.len()); } #[test] fn the_reorder_window_passes_every_accepted_frame_up_once_and_in_order() { for iteration in 0..FUZZ_ITERATIONS / 10 { let mut rng = Rng(1 << 40 | iteration); - let size = 1 + rng.below(64) as u16; + let size = 1 + rng.below(256) as u16; let first = rng.below(4096) as u32; let mut w = Window::new(Seq::new(first as u16), size).unwrap(); let mut base = first; diff --git a/lib/ieee80211/tests/refusals.rs b/lib/ieee80211/tests/refusals.rs index 2bfabc16045..a3472c0e563 100644 --- a/lib/ieee80211/tests/refusals.rs +++ b/lib/ieee80211/tests/refusals.rs @@ -60,6 +60,20 @@ fn block_ack(subtype: u8, rest: &[u8]) -> Refused { frame(&f) } +/// An HE Capabilities element whose PHY capabilities' first octet is `width_set`, with PPE +/// Thresholds present or not, and then `rest`. +fn he_capabilities(width_set: u8, ppe: bool, rest: &[u8]) -> Vec { + let mut phy = [0u8; 11]; + phy[0] = width_set; + phy[2] = if ppe { 0x80 } else { 0 }; + element(255, &[&[35][..], &[0; 6], &phy, &[0xFA, 0xFF, 0xFA, 0xFF], rest].concat()) +} + +/// A vendor-specific element under WMM's OUI and type. +fn wmm(after_type: &[u8]) -> Vec { + element(221, &[&[0x00, 0x50, 0xF2, 0x02][..], after_type].concat()) +} + fn window() -> Window { Window::new(Seq::new(100), 64).unwrap() } @@ -108,6 +122,10 @@ fn cases() -> Vec<(Refused, Refused)> { (ExtendedRsnLength, elements(&element(244, &[0x02, 0x20]))), (SecondaryChannelOffset, elements(&ht_operation(2))), (VhtReserved, elements(&element(192, &[4, 42, 0, 0xFF, 0xFF]))), + (PpeThresholds, elements(&he_capabilities(0, true, &[0b0001_1001, 0, 0]))), + (WmmVersion, elements(&wmm(&[0, 2, 0]))), + (WmmAifsn, elements(&wmm(&[[1, 1, 0, 0].as_slice(), &[0x01, 0xA4, 0, 0], &[0x27, 0xA4, 0, 0], &[0x42, 0x43, 94, 0], &[0x62, 0x32, 47, 0]].concat()))), + (WmmAccessCategory, elements(&wmm(&[[1, 1, 0, 0].as_slice(), &[0x03, 0xA4, 0, 0], &[0x27, 0xA4, 0, 0], &[0x42, 0x43, 94, 0], &[0x42, 0x32, 47, 0]].concat()))), (BlockAckVariant, block_ack(8, &[0x00, 0x00, 0x00, 0x00])), (BitmapSize, block_ack(9, &[[0x04, 0x00, 0x01, 0x00].as_slice(), &[0xFF; 8]].concat())), (ControlLength, block_ack(8, &[0x04, 0x00, 0x00, 0x00, 0x00])), @@ -119,7 +137,7 @@ fn cases() -> Vec<(Refused, Refused)> { (TranslatedType, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x81, 0x37])), (LengthField, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x05, 0xFF])), (ExtendedIvClear, refusal(CcmpHeader::split(&[1, 0, 0, 0, 0, 0, 0, 0]))), - (WindowSize, refusal(Window::::new(Seq::new(0), 65))), + (WindowSize, refusal(Window::::new(Seq::new(0), 257))), (OldSequence, refusal(window().receive(Seq::new(100 + 2048), 0, &mut Vec::new()))), (DuplicateSequence, { let mut w = window(); @@ -140,6 +158,69 @@ fn every_refusal_has_a_hostile_input_that_produces_it() { assert_eq!(all.difference(&named).collect::>(), Vec::<&Refused>::new()); } +#[test] +fn an_he_capabilities_element_is_as_long_as_its_widths_and_ppe_thresholds_say() { + let parse = |list: Vec| Elements::parse(&list).map(|e| e.he_capabilities.map(|h| (h.mcs_160.is_some(), h.mcs_80p80.is_some(), h.ppe_thresholds.map(<[u8]>::len)))); + assert_eq!(parse(he_capabilities(0, false, &[])), Ok(Some((false, false, None)))); + assert_eq!(parse(he_capabilities(0x18, false, &[0; 8])), Ok(Some((true, true, None)))); + assert_eq!(parse(he_capabilities(0x08, false, &[0; 8])), Err(Refused::ElementLength)); + assert_eq!(parse(he_capabilities(0x18, false, &[0; 7])), Err(Refused::ElementLength)); + // Two streams and two RU sizes: 7 + 2 × 2 × 6 = 31 bits, four octets. + assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0])), Ok(Some((false, false, Some(4))))); + assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0, 0])), Err(Refused::ElementLength)); +} + +#[test] +fn an_he_operation_element_is_as_long_as_its_present_bits_say() { + let op = |parameters: u32, rest: &[u8]| { + let p = parameters.to_le_bytes(); + element(255, &[&[36, p[0], p[1], p[2], 0x05, 0xFC, 0xFF][..], rest].concat()) + }; + let list = op(1 << 17, &[37, 0x02, 39, 0, 6]); + let six = Elements::parse(&list).unwrap().he_operation.unwrap(); + assert_eq!(six.six_ghz.map(|s| (s.primary_channel, s.center_segment0)), Some((37, 39))); + assert_eq!(six.bss_color & 0x3F, 5); + let list = op(1 << 14 | 1 << 15 | 1 << 17, &[1, 42, 0, 3, 37, 2, 39, 0, 6]); + assert_eq!(Elements::parse(&list).unwrap().he_operation.unwrap().vht, Some([1, 42, 0])); + assert_eq!(elements(&op(1 << 14, &[1, 42])), Refused::ElementLength); + assert_eq!(elements(&op(0, &[0])), Refused::ElementLength); +} + +#[test] +fn a_wmm_information_element_and_another_wmm_subtype_are_read_and_skipped() { + use toyos_ieee80211::element::Wmm; + let wmm_of = |list: Vec| Elements::parse(&list).unwrap().wmm; + assert_eq!(wmm_of(wmm(&[0, 1, 0x80])), Some(Wmm::Information { qos_info: 0x80 })); + assert_eq!(wmm_of(wmm(&[2, 1, 0xFF])), None); + assert_eq!(elements(&wmm(&[0, 1])), Refused::ElementLength); + assert_eq!(elements(&[wmm(&[0, 1, 0]), wmm(&[0, 1, 0])].concat()), Refused::DuplicateElement); +} + +#[test] +fn an_he_block_ack_bitmap_is_as_long_as_its_fragment_number_says() { + let ba = |fragment: u8, bitmap: &[u8]| { + let mut f = vec![0x94, 0, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend([0x04, 0x00, fragment | 0x10, 0x00]); + f.extend(bitmap); + match Frame::parse(&f)? { + Frame::BlockAck(b) => Ok(b), + other => panic!("not a BlockAck: {other:?}"), + } + }; + for (fragment, octets) in [(0b0000, 8), (0b0010, 32), (0b0100, 16), (0b0110, 4)] { + let mut bitmap = vec![0; octets]; + bitmap[octets - 1] = 0x80; + let b = ba(fragment, &bitmap).unwrap(); + let last = Seq::new(1).add(u16::try_from(octets * 8 - 1).unwrap()); + assert!(b.acked(last) && !b.acked(last.add(1)) && !b.acked(Seq::new(1)), "{octets} octets"); + assert_eq!(ba(fragment, &bitmap[1..]).err(), Some(Refused::Truncated)); + } + assert_eq!(ba(0b1000, &[0; 8]).err(), Some(Refused::BitmapSize)); + assert_eq!(ba(0b0001, &[0; 8]).err(), Some(Refused::BitmapSize)); +} + #[test] fn every_prefix_of_a_frame_is_refused_as_truncated_short_of_its_header() { let f = management(8, &beacon(&element(0, b"net"))); diff --git a/lib/ieee80211/tests/reorder.rs b/lib/ieee80211/tests/reorder.rs index 8ed54ba968f..080450aadb4 100644 --- a/lib/ieee80211/tests/reorder.rs +++ b/lib/ieee80211/tests/reorder.rs @@ -100,8 +100,21 @@ fn the_window_runs_across_the_end_of_the_sequence_space() { } #[test] -fn a_window_of_one_to_sixty_four_is_made_and_no_other() { +fn a_window_of_one_to_two_hundred_fifty_six_is_made_and_no_other() { assert!(Window::::new(Seq::new(0), 1).is_ok()); - assert!(Window::::new(Seq::new(0), 64).is_ok()); + assert!(Window::::new(Seq::new(0), 256).is_ok()); assert_eq!(Window::::new(Seq::new(0), 0).err(), Some(Refused::WindowSize)); } + +#[test] +fn an_he_window_holds_two_hundred_fifty_six_frames_behind_a_hole() { + let mut w = window(4000, 256); + for seq in 4001..4096 { + assert_eq!(receive(&mut w, seq), []); + } + for seq in 0..160 { + assert_eq!(receive(&mut w, seq), []); + } + let all: Vec = (4000..4096).chain(0..160).collect(); + assert_eq!(receive(&mut w, 4000), all); +} From 382b85167d8ca1fed0b86e874112d607e5bd7ef6 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 11:54:58 +0200 Subject: [PATCH 4/4] toyos-ieee80211: PPE Thresholds Present is B55, old and duplicate frames are a window outcome Round 3 of #924's review and the orchestrator's rulings on it. - HE Capabilities reads PPE Thresholds Present from PHY Capabilities B55 (octet 6, bit 7), not B23, which is Partial Bandwidth UL MU-MIMO: an AP setting either bit alone lost its beacon. The 160 and 80+80 MCS sets are named by their PHY bits, B3 and B4 (Channel Width Set B2 and B3). - HT, VHT and HE Capabilities and Operation ignore octets past their fields and refuse fewer, as the RSNE does; WMM keeps its exact length. - Window::receive answers Received::{Taken, Old, Duplicate}: a retransmitted frame is ordinary traffic, not a refusal, and OldSequence and DuplicateSequence leave Refused. TranslatedType, an 802.3 SNAP frame 802.1H carries legitimately, is Unsupported. Class::Malformed keeps its contract. - The W1 entry of issues/there-is-no-wifi.md records the check of every reading against 802.11-2020, 802.11ax-2021 and WMM v1.2, with its owner and exit. - issues/a-fragmented-wifi-frame-is-dropped.md says how the loss happens: each fragment is acknowledged before the parser drops it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- issues/a-fragmented-wifi-frame-is-dropped.md | 6 ++- issues/there-is-no-wifi.md | 42 +++++++++++++++ lib/ieee80211/src/element/he.rs | 23 ++++---- lib/ieee80211/src/element/ht.rs | 17 +++--- lib/ieee80211/src/lib.rs | 8 ++- lib/ieee80211/src/reorder.rs | 23 ++++++-- lib/ieee80211/tests/fuzz.rs | 15 +++--- lib/ieee80211/tests/refusals.rs | 56 ++++++++++++-------- lib/ieee80211/tests/reorder.rs | 20 +++++-- 9 files changed, 141 insertions(+), 69 deletions(-) diff --git a/issues/a-fragmented-wifi-frame-is-dropped.md b/issues/a-fragmented-wifi-frame-is-dropped.md index 383c04aa742..a2c670d4b14 100644 --- a/issues/a-fragmented-wifi-frame-is-dropped.md +++ b/issues/a-fragmented-wifi-frame-is-dropped.md @@ -20,8 +20,10 @@ 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 drops part of the station's traffic, retry by retry, -with nothing upstream told why. +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`. diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 27f0fb8b2bf..7873c9da973 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -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 diff --git a/lib/ieee80211/src/element/he.rs b/lib/ieee80211/src/element/he.rs index 9cee877cea5..11aa0f184a7 100644 --- a/lib/ieee80211/src/element/he.rs +++ b/lib/ieee80211/src/element/he.rs @@ -1,5 +1,6 @@ //! HE Capabilities (802.11ax-2021 §9.4.2.248) and HE Operation (§9.4.2.249), Element ID Extension -//! 35 and 36. Their lengths follow from bits inside them, and any other length is refused. +//! 35 and 36. Their lengths follow from bits inside them; a shorter element is refused, and octets +//! past those fields are ignored, so a field a later amendment appends costs no beacon. use crate::read::Reader; use crate::Refused; @@ -10,11 +11,11 @@ pub struct HeCapabilities<'a> { pub phy: [u8; 11], /// Rx and Tx HE-MCS maps for 80 MHz and narrower. pub mcs_80: [u8; 4], - /// For 160 MHz, present when the Channel Width Set's bit 3 is. + /// For 160 MHz, present when PHY Capabilities B3 (Channel Width Set B2) is set. pub mcs_160: Option<[u8; 4]>, - /// For 80+80 MHz, present when the Channel Width Set's bit 4 is. + /// For 80+80 MHz, present when PHY Capabilities B4 (Channel Width Set B3) is set. pub mcs_80p80: Option<[u8; 4]>, - /// Present when the PHY capabilities' bit 23 is. + /// Present when PHY Capabilities B55, PPE Thresholds Present, is set. pub ppe_thresholds: Option<&'a [u8]>, } @@ -24,14 +25,11 @@ impl<'a> HeCapabilities<'a> { let length = |_| Refused::ElementLength; let mac = r.array().map_err(length)?; let phy: [u8; 11] = r.array().map_err(length)?; - let [width_set, _, ppe_octet, ..] = phy; + let [phy0, _, _, _, _, _, phy6, ..] = phy; let mcs_80 = r.array().map_err(length)?; - let mcs_160 = if width_set & 1 << 3 != 0 { Some(r.array().map_err(length)?) } else { None }; - let mcs_80p80 = if width_set & 1 << 4 != 0 { Some(r.array().map_err(length)?) } else { None }; - let ppe_thresholds = if ppe_octet & 1 << 7 != 0 { Some(ppe(&mut r)?) } else { None }; - if !r.is_empty() { - return Err(Refused::ElementLength); - } + let mcs_160 = if phy0 & 1 << 3 != 0 { Some(r.array().map_err(length)?) } else { None }; + let mcs_80p80 = if phy0 & 1 << 4 != 0 { Some(r.array().map_err(length)?) } else { None }; + let ppe_thresholds = if phy6 & 1 << 7 != 0 { Some(ppe(&mut r)?) } else { None }; Ok(Self { mac, phy, mcs_80, mcs_160, mcs_80p80, ppe_thresholds }) } } @@ -91,9 +89,6 @@ impl HeOperation { } else { None }; - if !r.is_empty() { - return Err(Refused::ElementLength); - } Ok(Self { parameters, bss_color, basic_mcs, vht, max_cohosted_bssid, six_ghz }) } } diff --git a/lib/ieee80211/src/element/ht.rs b/lib/ieee80211/src/element/ht.rs index 3af9d51fb0d..e3f26ce5c83 100644 --- a/lib/ieee80211/src/element/ht.rs +++ b/lib/ieee80211/src/element/ht.rs @@ -1,12 +1,13 @@ //! HT Capabilities (§9.4.2.55), HT Operation (§9.4.2.56), VHT Capabilities (§9.4.2.157) and VHT -//! Operation (§9.4.2.158): fixed-length elements, refused at any other length or where a field -//! holds a value the standard reserves. +//! Operation (§9.4.2.158): fixed-length elements, refused when shorter or where a field holds a +//! value the standard reserves. Octets past the fixed fields are ignored, so a field a later +//! amendment appends costs no beacon. use crate::read::Reader; use crate::Refused; -fn exact(body: &[u8]) -> Result<[u8; N], Refused> { - body.try_into().map_err(|_| Refused::ElementLength) +fn fields(body: &[u8]) -> Result<[u8; N], Refused> { + body.first_chunk().copied().ok_or(Refused::ElementLength) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -22,7 +23,7 @@ pub struct HtCapabilities { impl HtCapabilities { pub(super) fn parse(body: &[u8]) -> Result { - let bytes: [u8; 26] = exact(body)?; + let bytes: [u8; 26] = fields(body)?; let mut r = Reader::new(&bytes); Ok(Self { info: r.u16()?, @@ -55,7 +56,7 @@ pub struct HtOperation { impl HtOperation { pub(super) fn parse(body: &[u8]) -> Result { - let bytes: [u8; 22] = exact(body)?; + let bytes: [u8; 22] = fields(body)?; let mut r = Reader::new(&bytes); let primary_channel = r.u8()?; let info: [u8; 5] = r.array()?; @@ -81,7 +82,7 @@ pub struct VhtCapabilities { impl VhtCapabilities { pub(super) fn parse(body: &[u8]) -> Result { - let bytes: [u8; 12] = exact(body)?; + let bytes: [u8; 12] = fields(body)?; let mut r = Reader::new(&bytes); let caps = Self { info: r.u32()?, @@ -119,7 +120,7 @@ pub struct VhtOperation { impl VhtOperation { pub(super) fn parse(body: &[u8]) -> Result { - let [width, center_segment0, center_segment1, m0, m1] = exact(body)?; + let [width, center_segment0, center_segment1, m0, m1] = fields(body)?; let width = match width { 0 => VhtChannelWidth::TwentyOrForty, 1 => VhtChannelWidth::Eighty, diff --git a/lib/ieee80211/src/lib.rs b/lib/ieee80211/src/lib.rs index 97b8f31cc2a..57aa8172565 100644 --- a/lib/ieee80211/src/lib.rs +++ b/lib/ieee80211/src/lib.rs @@ -45,7 +45,7 @@ pub enum Class { macro_rules! refusals { ($($variant:ident = $class:ident, $why:literal;)*) => { - /// Why a frame, element or sequence number was not accepted. + /// Why a frame or element was not accepted, or a reordering buffer not made. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Refused { $(#[doc = $why] $variant,)* @@ -110,7 +110,7 @@ refusals! { WmmAccessCategory = Malformed, "a WMM Parameter element that does not give each access category once"; // Control frames, §9.3.1. BlockAckVariant = Unsupported, "a block-ack variant other than Compressed"; - BitmapSize = Unsupported, "a Fragment Number giving a fragment level or a bitmap other than 32, 64, 128 or 256 bits"; + BitmapSize = Unsupported, "a block-ack Fragment Number this station does not handle: a fragment level, or a bitmap other than 32, 64, 128 or 256 bits"; ControlLength = Malformed, "a block-ack frame whose length its variant does not give"; // Data, §9.3.2 and §12.5.3. SubframeLength = Malformed, "an A-MSDU subframe whose length runs past the MPDU"; @@ -118,13 +118,11 @@ refusals! { SnapHeaderAsAddress = Malformed, "an A-MSDU whose first destination is an LLC/SNAP header"; NotSnap = Unsupported, "an MSDU that is not LLC/SNAP"; UnknownOui = Unsupported, "an LLC/SNAP header with an OUI other than RFC 1042's or bridge-tunnel's"; - TranslatedType = Malformed, "an RFC 1042 header carrying a type 802.1H sends by bridge-tunnel"; + TranslatedType = Unsupported, "an RFC 1042 header carrying a type 802.1H sends by bridge-tunnel"; LengthField = Unsupported, "an EtherType below 0x0600, which is an IEEE 802.3 length"; ExtendedIvClear = Malformed, "a CCMP header without its Ext IV bit"; // The reordering buffer, §10.25.6. WindowSize = Malformed, "a reordering buffer of 0 or more than 256 MPDUs"; - OldSequence = Malformed, "a sequence number before the window"; - DuplicateSequence = Malformed, "a sequence number already held in the window"; } impl core::fmt::Display for Refused { diff --git a/lib/ieee80211/src/reorder.rs b/lib/ieee80211/src/reorder.rs index 495720ed448..6e90c4959ea 100644 --- a/lib/ieee80211/src/reorder.rs +++ b/lib/ieee80211/src/reorder.rs @@ -3,7 +3,8 @@ //! //! A frame in the window is held, and the run from the window's start passes up. A frame past //! the window's end, within half the sequence space, moves the window to end at it, passing up -//! everything held before the new start. One further back than that is old and refused. A +//! everything held before the new start. One further back than that is old, and one already held +//! is a duplicate: both are dropped, the ordinary fate of a retransmission. A //! BlockAckReq's Starting Sequence Number moves the start forward as a frame would, and so does //! a device's release or a timeout, through [`Window::release_to`]. @@ -16,6 +17,18 @@ use crate::{Refused, Seq}; pub const MAX_SIZE: u16 = 256; const HALF: u16 = Seq::MODULUS / 2; +/// What became of a received frame. +#[must_use] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Received { + /// Held, or passed up with the run it completed. + Taken, + /// Dropped: before the window. + Old, + /// Dropped: its sequence number is already held. + Duplicate, +} + pub struct Window { start: Seq, /// Slot `(head + n) % size` holds sequence number `start + n`. @@ -41,10 +54,10 @@ impl Window { /// Take `item`, received with sequence number `seq`, and pass up into `out` every item it /// puts in order. - pub fn receive(&mut self, seq: Seq, item: T, out: &mut Vec) -> Result<(), Refused> { + pub fn receive(&mut self, seq: Seq, item: T, out: &mut Vec) -> Received { let ahead = seq.since(self.start); if ahead >= HALF { - return Err(Refused::OldSequence); + return Received::Old; } // A frame past the window's end becomes its end: the start moves to `size - 1` before it. if let Some(past) = ahead.checked_sub(self.size.get().saturating_sub(1)).filter(|&p| p > 0) { @@ -53,11 +66,11 @@ impl Window { let offset = usize::from(seq.since(self.start)); let slot = self.slot(offset); if slot.is_some() { - return Err(Refused::DuplicateSequence); + return Received::Duplicate; } *slot = Some(item); self.pass_run(out); - Ok(()) + Received::Taken } /// Move the window's start forward to `ssn`, passing up every held item before it, then the diff --git a/lib/ieee80211/tests/fuzz.rs b/lib/ieee80211/tests/fuzz.rs index 190382bcf58..f1052043d96 100644 --- a/lib/ieee80211/tests/fuzz.rs +++ b/lib/ieee80211/tests/fuzz.rs @@ -17,7 +17,7 @@ use toyos_ieee80211::data::{llc_snap, msdus, CcmpHeader, Ethernet}; use toyos_ieee80211::element::Elements; use toyos_ieee80211::frame::Frame; use toyos_ieee80211::mgmt::Body; -use toyos_ieee80211::reorder::Window; +use toyos_ieee80211::reorder::{Received, Window}; use toyos_ieee80211::{Address, Refused, Seq}; const FUZZ_ITERATIONS: u64 = 20_000; @@ -71,12 +71,13 @@ fn draw_element(rng: &mut Rng) -> Vec { 11 => element(192, &[rng.below(5) as u8, 42, 0, 0xFC, 0xFF]), 12 => element(127, &rng.bytes(10)), 13 => { - let width_set = [0, 0x08, 0x18, 0x10][rng.below(4)]; + let phy0 = [0, 0x08, 0x18, 0x10][rng.below(4)]; let ppe = rng.chance(40); let mut body = vec![35]; body.extend((0..6).map(|_| rng.next() as u8)); - body.extend([width_set, 0, if ppe { 0x80 } else { 0 }, 0, 0, 0, 0, 0, 0, 0, 0]); - body.extend(vec![0xFA; 4 + 4 * (width_set.count_ones() as usize)]); + // PHY Capabilities: B3 and B4 in octet 0, PPE Thresholds Present (B55) in octet 6. + body.extend([phy0, 0, rng.next() as u8, 0, 0, 0, if ppe { 0x80 } else { 0 }, 0, 0, 0, 0]); + body.extend(vec![0xFA; 4 + 4 * (phy0.count_ones() as usize)]); if ppe { let first = rng.next() as u8 & 0x7F; let bits = 7 + 6 * ((first & 7) as usize + 1) * (first >> 3).count_ones() as usize; @@ -277,10 +278,8 @@ fn the_reorder_window_passes_every_accepted_frame_up_once_and_in_order() { continue; } let position = (base + rng.below(size as usize * 2) as u32).saturating_sub(rng.below(size as usize) as u32); - match w.receive(Seq::new(position as u16), position, &mut out) { - Ok(()) => accepted += 1, - Err(Refused::OldSequence | Refused::DuplicateSequence) => {} - Err(other) => panic!("iteration {iteration}: {other:?}"), + if w.receive(Seq::new(position as u16), position, &mut out) == Received::Taken { + accepted += 1; } base = base.max(position.saturating_sub(size as u32 - 1)); } diff --git a/lib/ieee80211/tests/refusals.rs b/lib/ieee80211/tests/refusals.rs index a3472c0e563..d417d2f6507 100644 --- a/lib/ieee80211/tests/refusals.rs +++ b/lib/ieee80211/tests/refusals.rs @@ -31,7 +31,7 @@ fn country(body: &[u8]) -> Refused { elements(&element(7, body)) } -/// A header of `fc` and 22 octets of zeros: two addresses' worth past Duration and the third. +/// Frame Control `fc`, then 22 zero octets: Duration, three addresses, Sequence Control. fn header(fc: [u8; 2]) -> Vec { let mut f = fc.to_vec(); f.resize(24, 0); @@ -60,12 +60,12 @@ fn block_ack(subtype: u8, rest: &[u8]) -> Refused { frame(&f) } -/// An HE Capabilities element whose PHY capabilities' first octet is `width_set`, with PPE -/// Thresholds present or not, and then `rest`. -fn he_capabilities(width_set: u8, ppe: bool, rest: &[u8]) -> Vec { +/// An HE Capabilities element whose PHY Capabilities octet 0 is `phy0`, with PPE Thresholds +/// Present (B55) or not, and then `rest`. +fn he_capabilities(phy0: u8, ppe: bool, rest: &[u8]) -> Vec { let mut phy = [0u8; 11]; - phy[0] = width_set; - phy[2] = if ppe { 0x80 } else { 0 }; + phy[0] = phy0; + phy[6] = if ppe { 0x80 } else { 0 }; element(255, &[&[35][..], &[0; 6], &phy, &[0xFA, 0xFF, 0xFA, 0xFF], rest].concat()) } @@ -74,10 +74,6 @@ fn wmm(after_type: &[u8]) -> Vec { element(221, &[&[0x00, 0x50, 0xF2, 0x02][..], after_type].concat()) } -fn window() -> Window { - Window::new(Seq::new(100), 64).unwrap() -} - /// One hostile input per refusal. fn cases() -> Vec<(Refused, Refused)> { use Refused::*; @@ -138,12 +134,6 @@ fn cases() -> Vec<(Refused, Refused)> { (LengthField, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x05, 0xFF])), (ExtendedIvClear, refusal(CcmpHeader::split(&[1, 0, 0, 0, 0, 0, 0, 0]))), (WindowSize, refusal(Window::::new(Seq::new(0), 257))), - (OldSequence, refusal(window().receive(Seq::new(100 + 2048), 0, &mut Vec::new()))), - (DuplicateSequence, { - let mut w = window(); - w.receive(Seq::new(105), 0, &mut Vec::new()).unwrap(); - refusal(w.receive(Seq::new(105), 1, &mut Vec::new())) - }), ] } @@ -159,19 +149,30 @@ fn every_refusal_has_a_hostile_input_that_produces_it() { } #[test] -fn an_he_capabilities_element_is_as_long_as_its_widths_and_ppe_thresholds_say() { +fn an_he_capabilities_element_is_at_least_as_long_as_its_widths_and_ppe_thresholds_say() { let parse = |list: Vec| Elements::parse(&list).map(|e| e.he_capabilities.map(|h| (h.mcs_160.is_some(), h.mcs_80p80.is_some(), h.ppe_thresholds.map(<[u8]>::len)))); assert_eq!(parse(he_capabilities(0, false, &[])), Ok(Some((false, false, None)))); assert_eq!(parse(he_capabilities(0x18, false, &[0; 8])), Ok(Some((true, true, None)))); - assert_eq!(parse(he_capabilities(0x08, false, &[0; 8])), Err(Refused::ElementLength)); + assert_eq!(parse(he_capabilities(0x08, false, &[0; 3])), Err(Refused::ElementLength)); assert_eq!(parse(he_capabilities(0x18, false, &[0; 7])), Err(Refused::ElementLength)); - // Two streams and two RU sizes: 7 + 2 × 2 × 6 = 31 bits, four octets. + // Two streams and two RU sizes: 7 + 2 × 2 × 6 = 31 bits, four octets; the fifth is ignored. assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0])), Ok(Some((false, false, Some(4))))); - assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0, 0])), Err(Refused::ElementLength)); + assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0, 0])), Ok(Some((false, false, Some(4))))); + assert_eq!(parse(he_capabilities(0x08, false, &[0; 8])), Ok(Some((true, false, None)))); +} + +#[test] +fn ppe_thresholds_are_present_under_phy_capabilities_b55_and_not_b23() { + // Four octets that would read as PPE Thresholds, after the MCS set. + let mut b23 = he_capabilities(0, false, &[0xFF; 4]); + // Element ID, length, extension ID and 6 MAC octets: PHY octet 2 is at 11. + b23[11] = 0x80; + assert_eq!(Elements::parse(&b23).unwrap().he_capabilities.unwrap().ppe_thresholds, None); + assert_eq!(elements(&he_capabilities(0, true, &[])), Refused::PpeThresholds); } #[test] -fn an_he_operation_element_is_as_long_as_its_present_bits_say() { +fn an_he_operation_element_is_at_least_as_long_as_its_present_bits_say() { let op = |parameters: u32, rest: &[u8]| { let p = parameters.to_le_bytes(); element(255, &[&[36, p[0], p[1], p[2], 0x05, 0xFC, 0xFF][..], rest].concat()) @@ -183,7 +184,18 @@ fn an_he_operation_element_is_as_long_as_its_present_bits_say() { let list = op(1 << 14 | 1 << 15 | 1 << 17, &[1, 42, 0, 3, 37, 2, 39, 0, 6]); assert_eq!(Elements::parse(&list).unwrap().he_operation.unwrap().vht, Some([1, 42, 0])); assert_eq!(elements(&op(1 << 14, &[1, 42])), Refused::ElementLength); - assert_eq!(elements(&op(0, &[0])), Refused::ElementLength); + assert_eq!(Elements::parse(&op(0, &[0])).map(|e| e.he_operation.map(|o| o.vht)), Ok(Some(None))); + assert_eq!(elements(&element(255, &[36, 0, 0, 0, 0x05, 0xFC])), Refused::ElementLength); +} + +#[test] +fn ht_and_vht_elements_ignore_octets_past_their_fields_and_refuse_fewer() { + let ht_operation = |len: usize| element(61, &vec![0; len]); + assert!(Elements::parse(&ht_operation(23)).unwrap().ht_operation.is_some()); + assert_eq!(elements(&ht_operation(21)), Refused::ElementLength); + let vht_operation = |len: usize| element(192, &[&[1, 42, 0, 0xFC, 0xFF][..], &[0; 2]].concat()[..len]); + assert!(Elements::parse(&vht_operation(7)).unwrap().vht_operation.is_some()); + assert_eq!(elements(&vht_operation(4)), Refused::ElementLength); } #[test] diff --git a/lib/ieee80211/tests/reorder.rs b/lib/ieee80211/tests/reorder.rs index 080450aadb4..20d079ad348 100644 --- a/lib/ieee80211/tests/reorder.rs +++ b/lib/ieee80211/tests/reorder.rs @@ -1,6 +1,6 @@ //! The receive reordering buffer against §10.25.6's rules, one rule per test. -use toyos_ieee80211::reorder::Window; +use toyos_ieee80211::reorder::{Received, Window}; use toyos_ieee80211::{Refused, Seq}; fn window(start: u16, size: u16) -> Window { @@ -9,7 +9,7 @@ fn window(start: u16, size: u16) -> Window { fn receive(w: &mut Window, seq: u16) -> Vec { let mut out = Vec::new(); - w.receive(Seq::new(seq), seq, &mut out).unwrap(); + assert_eq!(w.receive(Seq::new(seq), seq, &mut out), Received::Taken); out } @@ -59,14 +59,24 @@ fn a_frame_a_whole_window_past_passes_up_everything_held() { } #[test] -fn a_frame_half_the_sequence_space_behind_is_old() { +fn a_frame_half_the_sequence_space_behind_is_dropped_as_old() { let mut w = window(10, 8); let mut out = Vec::new(); - assert_eq!(w.receive(Seq::new(9), 9, &mut out), Err(Refused::OldSequence)); - assert_eq!(w.receive(Seq::new(10 + 2048), 0, &mut out), Err(Refused::OldSequence)); + assert_eq!(w.receive(Seq::new(9), 9, &mut out), Received::Old); + assert_eq!(w.receive(Seq::new(10 + 2048), 0, &mut out), Received::Old); assert_eq!(receive(&mut w, 10 + 2047), []); } +#[test] +fn a_frame_already_held_is_dropped_as_a_duplicate() { + let mut w = window(10, 8); + receive(&mut w, 12); + let mut out = Vec::new(); + assert_eq!(w.receive(Seq::new(12), 99, &mut out), Received::Duplicate); + assert_eq!(receive(&mut w, 10), [10]); + assert_eq!(receive(&mut w, 11), [11, 12]); +} + #[test] fn a_release_passes_up_what_is_held_before_it_and_the_run_from_it() { let mut w = window(10, 8);