diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 7873c9da973..cc65ef596e8 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -230,48 +230,11 @@ 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. +- **Landed** in #924 and #930; the exit is met, the readings checked + against the text (#924, issuecomment-6173453493). +- **Owed:** the bridge-tunnel OUI 00-00-F8 and its EtherTypes 0x80F3 and + 0x8137 checked against ISO/IEC TR 11802-5:1997. Owner: the orchestrator. + Exit: checked, and fixed in `lib/ieee80211/src/data.rs` if contradicted. **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 @@ -324,7 +287,8 @@ W2. networks, an access point at the T14's bench serving a WPA2-Personal network and a WPA3-Personal one by hash-to-element, with PMF and HE, an instrument the orchestrator owns. Exit: a T14 row reads the firmware up, its MAC -address, and the test network seen. The network's name arrives per run +address, and the test network seen, its beacon's Country element read: a +missing pad (`CountryPad`) refuses the whole beacon. The network's name arrives per run through a test job holding `wifi`, so no shipping path reads the ESP. **W6, the first version: a full-rate join.** Depends on: W5 and its test @@ -335,6 +299,8 @@ networks, P4, P5, P6, P8. - each channel's limit as the tighter of the platform's table and the AP's country element; - a country element that would raise a limit; + - under operating classes, a country element with an invalid first + channel or a reserved class read up to it, as §10.22.3 has; - a platform with no table, refused by name. - **Exit, T14 rows:** - WPA2 and WPA3 joins, each with a lease, DNS and TCP; diff --git a/lib/ieee80211/src/control.rs b/lib/ieee80211/src/control.rs index 5351164df6a..90ed5070ac5 100644 --- a/lib/ieee80211/src/control.rs +++ b/lib/ieee80211/src/control.rs @@ -1,16 +1,20 @@ -//! 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. +//! BlockAckReq (§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`). +//! A BlockAck's bitmap is 64 or 256 bits long, as its Fragment Number subfield says (Table 9-38). +//! Acknowledging fragments, the subfield's B0, is refused, because a fragment is +//! (`Refused::Fragmented`). use crate::read::Reader; use crate::{Address, Refused, Seq}; -/// The BAR and BA Type value of the Compressed variant (Table 9-24, Table 9-28). +/// The BAR and BA Type value of the Compressed variant. const COMPRESSED: u16 = 2; +/// The BAR Type values Table 9-36 defines. +const BAR_TYPES: [u16; 5] = [1, COMPRESSED, 3, 6, 10]; +/// The BA Type values Table 9-37 defines. +const BA_TYPES: [u16; 7] = [1, COMPRESSED, 6, 7, 8, 10, 11]; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BlockAckReq { @@ -45,15 +49,23 @@ impl BlockAck { } /// 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> { +/// Fragment Number and sequence number, of a frame whose defined Type values are `types`. +fn compressed( + r: &mut Reader<'_>, + types: &[u16], +) -> 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). + // A VHT or HE transmitter may set the TA's group bit as a bandwidth signaling TA (§9.3.1.7.1, + // §10.6.12). 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 { + let kind = (control >> 1) & 0x0F; + if !types.contains(&kind) { + return Err(Refused::ReservedBlockAckType); + } + if kind != COMPRESSED { return Err(Refused::BlockAckVariant); } let [_, high] = control.to_le_bytes(); @@ -65,9 +77,9 @@ fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, u8, u8, Seq), Ref impl BlockAckReq { pub(crate) fn parse(after_duration: &[u8]) -> Result { let mut r = Reader::new(after_duration); - let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?; + let (ra, ta, tid, fragment, ssn) = compressed(&mut r, &BAR_TYPES)?; if fragment != 0 { - return Err(Refused::BitmapSize); + return Err(Refused::BlockAckFragmentNumber); } if !r.is_empty() { return Err(Refused::ControlLength); @@ -79,15 +91,16 @@ impl BlockAckReq { impl BlockAck { pub(crate) fn parse(after_duration: &[u8]) -> Result { let mut r = Reader::new(after_duration); - let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?; - // Bits 2 and 1 of the Fragment Number give the bitmap's length, bit 0 a fragment level. - let bits: u16 = match fragment { + let (ra, ta, tid, fragment, ssn) = compressed(&mut r, &BA_TYPES)?; + // B2–B1 of 0 is a 64-bit bitmap and 2 a 256-bit one; 1, 3 and B3 set are reserved. + let bits: u16 = match fragment & 0b1110 { 0b0000 => 64, - 0b0010 => 256, - 0b0100 => 128, - 0b0110 => 32, - _ => return Err(Refused::BitmapSize), + 0b0100 => 256, + _ => return Err(Refused::BlockAckFragmentNumber), }; + if fragment & 1 != 0 { + return Err(Refused::FragmentLevel); + } let mut bitmap = [0u64; 4]; let mut octets = r.take(usize::from(bits / 8))?.iter(); for word in &mut bitmap { diff --git a/lib/ieee80211/src/data.rs b/lib/ieee80211/src/data.rs index 6dfc8f02514..c14890226e7 100644 --- a/lib/ieee80211/src/data.rs +++ b/lib/ieee80211/src/data.rs @@ -1,6 +1,7 @@ -//! 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. +//! Data frame bodies: the CCMP header (§12.5.2.2), A-MSDU (§9.3.2.2), and translation between an +//! MSDU and an Ethernet II frame as §5.1.4 recommends, by ISO/IEC TR 11802-5:1997 (formerly IEEE +//! Std 802.1H): an RFC 1042 LLC/SNAP header, or the bridge-tunnel one for the two EtherTypes its +//! selective translation table sends that way. use crate::frame::Data; use crate::read::Reader; @@ -50,7 +51,8 @@ pub fn msdus<'a>(frame: &Data<'_>, body: &'a [u8]) -> Result, Refused> } // 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). + // flipped the bit on, which §10.11 forbids (Vanhoef, "Fragment and Forge", USENIX Security + // 2021, §3.2). if body.starts_with(&SNAP) { return Err(Refused::SnapHeaderAsAddress); } @@ -108,9 +110,11 @@ pub struct Ethernet<'a> { 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. +/// The EtherTypes the selective translation table 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). +/// The least type: IEEE Std 802.3-2022 §3.2.6 reads a Length/Type value of at most 1500 as a +/// length and one of at least 1536 (0x0600) as a type. const MIN_ETHERTYPE: u16 = 0x0600; pub const LLC_SNAP_LEN: usize = 8; @@ -148,7 +152,7 @@ pub fn llc_snap(ethertype: u16) -> Result<[u8; LLC_SNAP_LEN], Refused> { } /// 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). +/// Address 2 the source and Address 3 the destination (Table 9-60). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ToDs { pub bssid: Address, diff --git a/lib/ieee80211/src/element/country.rs b/lib/ieee80211/src/element/country.rs index 5a82f846a97..62a4016a1d2 100644 --- a/lib/ieee80211/src/element/country.rs +++ b/lib/ieee80211/src/element/country.rs @@ -1,5 +1,8 @@ -//! The Country element (§9.4.2.8): a country string, then triplets, then a pad octet of 0 where -//! one makes the length even. +//! The Country element (§9.4.2.7): a country string, then triplets, then a pad octet of 0 exactly +//! where one makes the length even. +//! +//! Channel numbers are not checked: their meaning depends on the operating class (Annex E), and +//! §10.22.3 has a station ignore the rest of the element at an invalid one, not refuse the frame. use crate::Refused; @@ -20,7 +23,6 @@ pub enum Triplet { /// 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 { @@ -41,11 +43,8 @@ impl<'a> Country<'a> { 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); - } + if !body.len().is_multiple_of(2) { + return Err(Refused::CountryPad); } Ok(Self { string: [a, b, environment], triplets }) } diff --git a/lib/ieee80211/src/element/he.rs b/lib/ieee80211/src/element/he.rs index 11aa0f184a7..d6eff5da1ff 100644 --- a/lib/ieee80211/src/element/he.rs +++ b/lib/ieee80211/src/element/he.rs @@ -1,6 +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; a shorter element is refused, and octets -//! past those fields are ignored, so a field a later amendment appends costs no beacon. +//! HE Capabilities (§9.4.2.247) and HE Operation (§9.4.2.248), Element ID Extension 35 and 36. +//! Their lengths follow from bits inside them; a shorter element is refused, and octets past those +//! fields are ignored, as §10.28.8 requires of an extensible element. use crate::read::Reader; use crate::Refused; @@ -35,7 +35,7 @@ impl<'a> HeCapabilities<'a> { } /// 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). +/// stream and RU size, padded to whole octets (§9.4.2.247.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); @@ -62,7 +62,7 @@ pub struct HeOperation { #[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. + /// Channel Width in bits 0 and 1, Duplicate Beacon bit 2, Regulatory Info bits 3 to 6. pub control: u8, pub center_segment0: u8, pub center_segment1: u8, diff --git a/lib/ieee80211/src/element/ht.rs b/lib/ieee80211/src/element/ht.rs index e3f26ce5c83..3b5d10b9823 100644 --- a/lib/ieee80211/src/element/ht.rs +++ b/lib/ieee80211/src/element/ht.rs @@ -1,7 +1,7 @@ -//! 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 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. +//! HT Capabilities (§9.4.2.54), HT Operation (§9.4.2.55), VHT Capabilities (§9.4.2.156) and VHT +//! Operation (§9.4.2.157): fixed-length elements, refused when shorter or where a field holds a +//! value the standard reserves. Octets past the fixed fields are ignored, as §10.28.8 requires of +//! an extensible element. use crate::read::Reader; use crate::Refused; @@ -36,7 +36,7 @@ impl HtCapabilities { } } -/// Secondary Channel Offset (§9.4.2.56). +/// Secondary Channel Offset (§9.4.2.55). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SecondaryChannel { None, @@ -98,7 +98,7 @@ impl VhtCapabilities { } } -/// The VHT Operation Information's Channel Width (Table 9-274). +/// The VHT Operation Information's Channel Width (Table 9-316). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum VhtChannelWidth { TwentyOrForty, diff --git a/lib/ieee80211/src/element/mod.rs b/lib/ieee80211/src/element/mod.rs index 483bcf62cbc..2963ceac8f0 100644 --- a/lib/ieee80211/src/element/mod.rs +++ b/lib/ieee80211/src/element/mod.rs @@ -2,8 +2,8 @@ //! by, each checked against its definition and allowed once. //! //! 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. +//! skipped, as §10.28.6 and §10.28.7 have a receiver ignore what it does not know; one it types +//! that breaks its definition refuses the whole list. mod country; mod he; @@ -101,11 +101,10 @@ pub struct Rate { } impl Rate { - /// BSS membership selectors (Table 9-78). + /// BSS membership selectors (Table 9-131). 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; } @@ -142,7 +141,7 @@ pub struct Elements<'a> { pub wmm: 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.25). #[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 680ee23d7a7..717153fe66b 100644 --- a/lib/ieee80211/src/element/rsn.rs +++ b/lib/ieee80211/src/element/rsn.rs @@ -1,13 +1,14 @@ -//! The RSN element (§9.4.2.24) and the RSN Extension element (§9.4.2.241). +//! The RSN element (§9.4.2.23) and the RSN Extension element (§9.4.2.240). //! //! 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. +//! needs every one before it. Table 9-130 does not mark the RSNE extensible, so an octet past its +//! last field is refused. The RSNXE is extensible: octets past its field are ignored, as §10.28.8 +//! requires. 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.23.2, §9.4.2.23.3). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Suite(pub [u8; 4]); @@ -16,7 +17,7 @@ impl Suite { Self([0x00, 0x0F, 0xAC, kind]) } - // Cipher suites, Table 9-149. + // Cipher suites, Table 9-188. pub const TKIP: Self = Self::ieee(2); pub const CCMP_128: Self = Self::ieee(4); pub const BIP_CMAC_128: Self = Self::ieee(6); @@ -28,7 +29,7 @@ impl Suite { pub const BIP_GMAC_256: Self = Self::ieee(12); pub const BIP_CMAC_256: Self = Self::ieee(13); - // AKM suites, Table 9-151. + // AKM suites, Table 9-190. 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); @@ -111,6 +112,9 @@ impl<'a> Rsn<'a> { return Ok(rsn); } rsn.group_management = Some(Suite(r.array()?)); + if !r.is_empty() { + return Err(Refused::ElementLength); + } Ok(rsn) } @@ -147,7 +151,7 @@ impl<'a> Rsnx<'a> { self.0.get(n / 8).is_some_and(|octet| octet >> (n % 8) & 1 != 0) } - /// SAE Hash-to-Element, bit 5 (Table 9-321). + /// SAE Hash-to-Element, bit 5 (Table 9-373). pub fn sae_h2e(&self) -> bool { self.bit(5) } diff --git a/lib/ieee80211/src/element/wmm.rs b/lib/ieee80211/src/element/wmm.rs index 4ffbd3929e2..c4afb46a249 100644 --- a/lib/ieee80211/src/element/wmm.rs +++ b/lib/ieee80211/src/element/wmm.rs @@ -1,5 +1,5 @@ -//! 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. +//! The WMM Information and Parameter elements (Wi-Fi Alliance, WMM Specification v1.2.0, §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; diff --git a/lib/ieee80211/src/frame.rs b/lib/ieee80211/src/frame.rs index 9b4f3486bae..3af065827e7 100644 --- a/lib/ieee80211/src/frame.rs +++ b/lib/ieee80211/src/frame.rs @@ -55,7 +55,7 @@ pub struct Management<'a> { pub body: &'a [u8], } -/// To DS and From DS (Table 9-4). +/// A data frame's To DS and From DS (Table 9-3). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Ds { /// Station to station within one BSS. @@ -78,7 +78,7 @@ impl QosControl { low & 0x0F } - /// The Ack Policy subfield, bits 5 and 6 (Table 9-11). + /// The Ack Policy subfield, bits 5 and 6 (Table 9-10, Table 9-13). pub const fn ack_policy(self) -> u8 { let [low, _] = self.0.to_le_bytes(); (low >> 5) & 0b11 @@ -117,7 +117,7 @@ impl Data<'_> { self.addr2 } - /// The MSDU's destination (Table 9-30). + /// The MSDU's destination (Table 9-60). pub const fn da(&self) -> Address { match self.ds { Ds::Neither | Ds::FromDs => self.addr1, @@ -125,7 +125,7 @@ impl Data<'_> { } } - /// The MSDU's source (Table 9-30). + /// The MSDU's source (Table 9-60). pub const fn sa(&self) -> Address { match self.ds { Ds::Neither | Ds::ToDs => self.addr2, @@ -156,8 +156,10 @@ impl<'a> Frame<'a> { match (fc0 >> 2) & 0b11 { 0 => { let subtype = management_subtype(subtype)?; - if ds_bits != 0 { - return Err(Refused::ManagementWithDs); + match ds_bits { + 0 => {} + 1 => return Err(Refused::QosManagement), + _ => return Err(Refused::ManagementFromDs), } let da = r.address()?; let sa = r.address()?; @@ -182,6 +184,7 @@ impl<'a> Frame<'a> { 1 => match subtype { 8 => BlockAckReq::parse(r.rest()).map(Self::BlockAckReq), 9 => BlockAck::parse(r.rest()).map(Self::BlockAck), + 0 | 1 | 15 => Err(Refused::ReservedSubtype), _ => Err(Refused::UnhandledControl), }, 2 => { @@ -190,11 +193,11 @@ impl<'a> Frame<'a> { 4 => (false, true), 8 => (true, false), 12 => (true, true), - 13 => return Err(Refused::ReservedSubtype), - _ => return Err(Refused::ContentionFreeData), + 2 | 3 | 5..=7 | 13 => return Err(Refused::ReservedSubtype), + _ => return Err(Refused::UnhandledData), }; if order && !qos { - return Err(Refused::StrictlyOrdered); + return Err(Refused::HtcInNonQosData); } let addr1 = r.address()?; let addr2 = r.address()?; @@ -225,7 +228,8 @@ impl<'a> Frame<'a> { body: r.rest(), })) } - _ => Err(Refused::ExtensionFrame), + _ if subtype <= 1 => Err(Refused::ExtensionFrame), + _ => Err(Refused::ReservedSubtype), } } } diff --git a/lib/ieee80211/src/lib.rs b/lib/ieee80211/src/lib.rs index 57aa8172565..9138d5abbd0 100644 --- a/lib/ieee80211/src/lib.rs +++ b/lib/ieee80211/src/lib.rs @@ -1,6 +1,6 @@ -//! 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. +//! IEEE Std 802.11-2024 frames as a station meets them, with the Wi-Fi Alliance's WMM element +//! (WMM Specification v1.2.0): 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 @@ -70,16 +70,17 @@ macro_rules! refusals { } refusals! { - // The MAC header, 9.2. + // 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"; + ExtensionFrame = Unsupported, "a DMG or S1G Beacon, the frames of type Extension"; + ReservedSubtype = Malformed, "a type and subtype Table 9-1 reserves"; + QosManagement = Unsupported, "a QoS management frame: To DS set, From DS clear (Table 9-4)"; + ManagementFromDs = Malformed, "a management frame with From DS set, which Table 9-4 reserves"; 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"; + UnhandledData = Unsupported, "an EBCS Data frame, or a QoS data subtype with +CF-Ack or +CF-Poll"; + HtcInNonQosData = Malformed, "+HTC set in a data frame other than QoS Data or QoS Null (§9.2.4.1.10)"; Fragmented = Unsupported, "a fragment of an MSDU or MMPDU"; GroupTransmitter = Malformed, "a transmitter address with the group bit set"; // Management bodies, §9.3.3. @@ -95,9 +96,8 @@ refusals! { 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"; + CountryPad = Malformed, "a Country element of odd length, or 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"; @@ -109,19 +109,21 @@ refusals! { 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. + ReservedBlockAckType = Malformed, "a BAR or BA Type Table 9-36 or Table 9-37 reserves"; BlockAckVariant = Unsupported, "a block-ack variant other than Compressed"; - 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"; + BlockAckFragmentNumber = Malformed, "a BlockAckReq Fragment Number other than 0, or a BlockAck one Table 9-38 reserves"; + FragmentLevel = Unsupported, "a BlockAck acknowledging fragments: Fragment Number B0 set"; 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.2. 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 = 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"; + TranslatedType = Unsupported, "an RFC 1042 header carrying a type the selective translation table sends by bridge-tunnel"; + LengthField = Unsupported, "a Length/Type value below 0x0600, which IEEE Std 802.3-2022 §3.2.6 does not read as a type"; ExtendedIvClear = Malformed, "a CCMP header without its Ext IV bit"; - // The reordering buffer, §10.25.6. + // The reordering buffer, §10.25.6.6. WindowSize = Malformed, "a reordering buffer of 0 or more than 256 MPDUs"; } @@ -145,7 +147,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.3, §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 66898038c01..5921ceaedef 100644 --- a/lib/ieee80211/src/mgmt.rs +++ b/lib/ieee80211/src/mgmt.rs @@ -60,7 +60,7 @@ pub struct AssociationResponse<'a> { pub elements: &'a [u8], } -/// The Authentication Algorithm Number of SAE (Table 9-43). +/// The Authentication Algorithm Number of SAE (§9.4.1.1). pub const ALGORITHM_SAE: u16 = 3; pub const ALGORITHM_OPEN: u16 = 0; pub const STATUS_SUCCESS: u16 = 0; diff --git a/lib/ieee80211/src/reorder.rs b/lib/ieee80211/src/reorder.rs index 6e90c4959ea..4e15a3d763a 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.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 @@ -13,7 +13,7 @@ use core::num::{NonZeroU16, NonZeroUsize}; use crate::{Refused, Seq}; -/// The largest buffer an HE agreement negotiates (802.11ax-2021 §9.4.1.14); HT and VHT stop at 64. +/// The largest buffer an HE agreement negotiates (§10.25.2); HT and VHT stop at 64. pub const MAX_SIZE: u16 = 256; const HALF: u16 = Seq::MODULUS / 2; diff --git a/lib/ieee80211/tests/common/mod.rs b/lib/ieee80211/tests/common/mod.rs index 5af704c4f5f..7b48ef97e2b 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 Std 802.3-2022 §3.2.9, which 802.11's FCS is (§9.2.4.9). pub fn crc32(bytes: &[u8]) -> u32 { let mut crc = !0u32; for &b in bytes { diff --git a/lib/ieee80211/tests/fuzz.rs b/lib/ieee80211/tests/fuzz.rs index f1052043d96..3203e22cab4 100644 --- a/lib/ieee80211/tests/fuzz.rs +++ b/lib/ieee80211/tests/fuzz.rs @@ -43,8 +43,9 @@ fn draw_element(rng: &mut Rng) -> 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); + // The pad an odd length needs, sometimes left out or added where none is needed. + if (body.len() % 2 == 1) != rng.chance(10) { + body.push(0); } element(7, &body) } @@ -135,7 +136,7 @@ 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]); - let (fragment, octets) = if bar { (0, 0) } else { [(0, 8), (2, 32), (4, 16), (6, 4)][rng.below(4)] }; + let (fragment, octets) = if bar { (0, 0) } else { [(0, 8), (4, 32)][rng.below(2)] }; f.extend(((rng.below(4096) << 4) as u16 | fragment).to_le_bytes()); f.extend((0..octets).map(|_| rng.next() as u8)); f diff --git a/lib/ieee80211/tests/refusals.rs b/lib/ieee80211/tests/refusals.rs index d417d2f6507..9ab7d159e33 100644 --- a/lib/ieee80211/tests/refusals.rs +++ b/lib/ieee80211/tests/refusals.rs @@ -88,11 +88,12 @@ fn cases() -> Vec<(Refused, Refused)> { (ProtocolVersion, frame(&header([0x81, 0]))), (ExtensionFrame, frame(&header([0x0C, 0]))), (ReservedSubtype, frame(&header([0x70, 0]))), - (ManagementWithDs, frame(&header([0x80, 0x01]))), + (QosManagement, frame(&header([0xD0, 0x01]))), + (ManagementFromDs, frame(&header([0x80, 0x02]))), (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]))), + (UnhandledData, frame(&header([0x18, 0]))), + (HtcInNonQosData, frame(&header([0x08, 0x80]))), (Fragmented, frame(&fragmented)), (GroupTransmitter, frame(&group_sa)), (ZeroBeaconInterval, refusal(Body::parse(ManagementSubtype::Beacon, &[0; 12]))), @@ -108,7 +109,6 @@ fn cases() -> Vec<(Refused, Refused)> { (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]))), ( @@ -122,8 +122,10 @@ fn cases() -> Vec<(Refused, Refused)> { (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())), + (ReservedBlockAckType, block_ack(8, &[0x00, 0x00, 0x00, 0x00])), + (BlockAckVariant, block_ack(8, &[0x02, 0x00, 0x00, 0x00])), + (BlockAckFragmentNumber, block_ack(8, &[0x04, 0x00, 0x01, 0x00])), + (FragmentLevel, 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))), @@ -221,7 +223,7 @@ fn an_he_block_ack_bitmap_is_as_long_as_its_fragment_number_says() { other => panic!("not a BlockAck: {other:?}"), } }; - for (fragment, octets) in [(0b0000, 8), (0b0010, 32), (0b0100, 16), (0b0110, 4)] { + for (fragment, octets) in [(0b0000, 8), (0b0100, 32)] { let mut bitmap = vec![0; octets]; bitmap[octets - 1] = 0x80; let b = ba(fragment, &bitmap).unwrap(); @@ -229,8 +231,59 @@ fn an_he_block_ack_bitmap_is_as_long_as_its_fragment_number_says() { 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)); + // Table 9-38 reserves B2–B1 of 1 and 3 and B3 set; B0 is a fragment level. + for fragment in [0b0010, 0b0110, 0b1000, 0b1100, 0b0011] { + assert_eq!(ba(fragment, &[0; 32]).err(), Some(Refused::BlockAckFragmentNumber), "{fragment:#06b}"); + } + for fragment in [0b0001, 0b0101] { + assert_eq!(ba(fragment, &[0; 32]).err(), Some(Refused::FragmentLevel), "{fragment:#06b}"); + } +} + +#[test] +fn a_block_ack_type_is_reserved_or_another_variant_as_its_table_says() { + let bar = |kind: u8| block_ack(8, &[kind << 1, 0x00, 0x00, 0x00]); + let ba = |kind: u8| block_ack(9, &[[kind << 1, 0x00, 0x00, 0x00].as_slice(), &[0; 8]].concat()); + for kind in (0..16).filter(|&kind| kind != 2) { + let bar_wanted = match kind { + 1 | 3 | 6 | 10 => Refused::BlockAckVariant, + _ => Refused::ReservedBlockAckType, + }; + let ba_wanted = match kind { + 1 | 6 | 7 | 8 | 10 | 11 => Refused::BlockAckVariant, + _ => Refused::ReservedBlockAckType, + }; + assert_eq!(bar(kind), bar_wanted, "BAR Type {kind}"); + assert_eq!(ba(kind), ba_wanted, "BA Type {kind}"); + } +} + +#[test] +fn each_type_and_subtype_is_read_or_refused_as_table_9_1_says() { + use Refused::*; + for fc0 in (0..=0xFF_u8).step_by(4) { + let subtype = fc0 >> 4; + let wanted = match (fc0 >> 2 & 0b11, subtype) { + (0, 7 | 15) | (1, 0 | 1 | 15) | (2, 2 | 3 | 5..=7 | 13) | (3, 2..) => Some(ReservedSubtype), + (0, 6 | 9) => Some(UnhandledManagement), + (0, _) | (1, 8 | 9) | (2, 0 | 4 | 8 | 12) => None, + (1, _) => Some(UnhandledControl), + (2, _) => Some(UnhandledData), + (3, _) => Some(ExtensionFrame), + _ => unreachable!(), + }; + let mut f = header([fc0, 0]); + f.extend([0; 32]); + if fc0 >> 2 & 0b11 == 1 { + // A BlockAckReq or BlockAck of the Compressed variant and its 64-bit bitmap. + f.truncate(16); + f.extend([0x04, 0x00, 0x00, 0x00]); + if subtype == 9 { + f.extend([0; 8]); + } + } + assert_eq!(Frame::parse(&f).err(), wanted, "Frame Control {fc0:#04x}"); + } } #[test] @@ -259,26 +312,41 @@ fn a_malformed_country_element_is_refused_whichever_rule_it_breaks() { 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); + // One triplet makes the length even; a pad of 0 after it makes it odd. + assert_eq!(country(b"US \x01\x0b\x14\x00"), Refused::CountryPad); + // Two triplets make it odd, and they need the pad. + assert_eq!(country(b"US \x01\x0b\x14\x24\x04\x17"), Refused::CountryPad); } #[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"); + // Three triplets: an even length without a pad. A first channel of 200 is the last a subband + // triplet can name; its channels run on past it. + let list = element(7, b"DE \x01\x0d\x14\xc8\x0b\xf6\xc9\x51\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::Subband { first_channel: 200, channels: 11, max_power_dbm: -10 }, Triplet::Operating { extension_id: 201, class: 81, coverage_class: 0 }, ] ); + // 6 GHz channels 193 to 233 of operating class 131 (Table E-4), and the pad two triplets need. + let list = element(7, b"US\x04\xc9\x83\x00\xc1\x0b\x00\x00"); + let c = Elements::parse(&list).unwrap().country.unwrap(); + assert_eq!( + c.triplets().collect::>(), + [ + Triplet::Operating { extension_id: 201, class: 131, coverage_class: 0 }, + Triplet::Subband { first_channel: 193, channels: 11, max_power_dbm: 0 }, + ] + ); + // Channel numbers are the operating class's to judge, a first channel or count of 0 included. + assert!(Elements::parse(&element(7, b"US \x00\x00\x14")).unwrap().country.is_some()); } #[test] @@ -329,6 +397,14 @@ fn an_rsn_element_may_end_after_any_whole_field_and_not_inside_one() { let list = element(48, &full); let rsn = Elements::parse(&list).unwrap().rsn.unwrap(); assert!(rsn.mfp_required() && rsn.mfp_capable()); + // Table 9-130 does not mark the RSNE extensible: nothing follows its last field. + assert_eq!(elements(&element(48, &[&full[..], &[0]].concat())), Refused::ElementLength); +} + +#[test] +fn an_rsnxe_ignores_octets_past_its_field() { + let list = element(244, &[0x20, 0xFF]); + assert!(Elements::parse(&list).unwrap().rsnx.unwrap().sae_h2e()); } #[test] diff --git a/lib/ieee80211/tests/reorder.rs b/lib/ieee80211/tests/reorder.rs index 20d079ad348..c330c5dda78 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.6's rules, one rule per test. use toyos_ieee80211::reorder::{Received, Window}; use toyos_ieee80211::{Refused, Seq};