diff --git a/Cargo.toml b/Cargo.toml index 2bc16881..4d46cfea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ default-features = false [dev-dependencies] bincode = "1.3" criterion = "0.3" +proptest = "1.1.0" rand = "0.8" serde = "1" serde_json = "1" diff --git a/src/serdes/slice.rs b/src/serdes/slice.rs index 1c495ef5..0d12f097 100644 --- a/src/serdes/slice.rs +++ b/src/serdes/slice.rs @@ -26,8 +26,6 @@ use serde::{ Serializer, }, }; -use wyz::comu::Const; - use super::{ utils::TypeName, Field, @@ -149,11 +147,15 @@ where FIELDS, BitSeqVisitor::, Self, _>::new( |vec, head, bits| unsafe { - let addr = vec.as_ptr().into_address(); - let mut bv = BitVec::try_from_vec(vec).map_err(|_| { - BitSpan::::new(addr, head, bits) - .unwrap_err() - })?; + let live = vec.len().saturating_mul(bits_of::()); + let mut bv = BitVec::try_from_vec(vec) + .map_err(|_| BitSpanError::TooLong(live))?; + let fits = (head.into_inner() as usize) + .checked_add(bits) + .map_or(false, |need| need <= live); + if !fits { + return Err(BitSpanError::TooLong(bits)); + } bv.set_head(head); bv.set_len(bits); Ok(bv) @@ -214,7 +216,8 @@ where let bits = self.bits.take().ok_or_else(|| E::missing_field("bits"))?; let data = self.data.take().ok_or_else(|| E::missing_field("data"))?; - (self.func)(data, head, bits as usize).map_err(|_| todo!()) + (self.func)(data, head, bits as usize) + .map_err(|e| ::custom(format_args!("invalid BitSeq: {e:?}"))) } } diff --git a/tests/serde_deserialize.rs b/tests/serde_deserialize.rs new file mode 100644 index 00000000..fb4d8ccc --- /dev/null +++ b/tests/serde_deserialize.rs @@ -0,0 +1,120 @@ +//! Deserialization hardening tests for the dynamic `BitSeq` transport format +//! (`BitVec` / `BitBox`). +//! +//! A malicious or corrupt buffer can carry a `head` / `bits` pair that does not +//! fit the supplied `data` buffer. Such input must produce a deserialization +//! `Err`, never a panic or memory-unsafety abort. +//! +//! Acceptance invariant: a buffer is valid iff `head + bits <= data.len() * +//! bits_of::()`. +//! +//! The `head + bits` overflow cases previously aborted the process (`set_len` +//! assertion followed by a `Vec::from_raw_parts` UB precondition violation in +//! `Drop`); the deserializer now validates `head + bits` against the buffer +//! length and returns `Err`. + +#![cfg(all(feature = "std", feature = "serde"))] + +use bitvec::prelude::*; +use proptest::prelude::*; + +const BITS_PER_ELEM: u64 = 64; // `usize` on the 64-bit test targets + +/// Builds a bincode buffer for `BitVec` with the `head` and `bits` +/// wire fields overwritten. +/// +/// Wire layout (bincode, fixint LE): `order: &str` (u64 len + utf8) | `head: +/// BitIdx` (u8 width, u8 index) | `bits: u64` | `data: Vec` (u64 len + +/// len*8). +fn tampered(data_len: usize, head: u8, bits: u64) -> Vec { + let original: BitVec = + BitVec::repeat(false, data_len * BITS_PER_ELEM as usize); + let mut bytes = bincode::serialize(&original).unwrap(); + + let order_len = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize; + let head_index_offset = 8 + order_len + 1; + let bits_offset = 8 + order_len + 2; + + bytes[head_index_offset] = head; + bytes[bits_offset..bits_offset + 8].copy_from_slice(&bits.to_le_bytes()); + bytes +} + +fn deser(bytes: &[u8]) -> Result, ()> { + bincode::deserialize::>(bytes).map_err(|_| ()) +} + +#[test] +fn huge_bits_is_rejected() { + // Original report: `bits` far above capacity. Fixed; guards the regression. + assert!(deser(&tampered(1, 0, 1_000_000)).is_err()); +} + +#[test] +fn bits_one_past_capacity_is_rejected() { + assert!(deser(&tampered(1, 0, BITS_PER_ELEM + 1)).is_err()); +} + +#[test] +fn head_plus_bits_within_capacity_roundtrips() { + let bv = deser(&tampered(1, 32, 32)).expect("32 + 32 == 64 fits"); + assert_eq!(bv.len(), 32); +} + +#[test] +fn head_plus_bits_past_capacity_is_rejected() { + // head=32, bits=64 => needs 96 bits, only 64 available. + assert!(deser(&tampered(1, 32, BITS_PER_ELEM)).is_err()); +} + +#[test] +fn head_plus_bits_one_past_capacity_is_rejected() { + assert!(deser(&tampered(2, 32, 2 * BITS_PER_ELEM - 31)).is_err()); +} + +#[test] +fn empty_data_nonzero_head_is_rejected() { + // Zero-length output, empty buffer, but head != 0 => head + bits > 0. + assert!(deser(&tampered(0, 1, 0)).is_err()); +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(2048))] + + /// Deserialization never panics, and accepts a buffer iff `head + bits` + /// fits the data capacity. + #[test] + fn deserialize_matches_capacity_invariant( + data_len in 0usize ..= 4, + head in 0u8 ..= 63, + bits in 0u64 ..= 320, + ) { + let fits = head as u64 + bits <= data_len as u64 * BITS_PER_ELEM; + prop_assert_eq!(deser(&tampered(data_len, head, bits)).is_ok(), fits); + } + + /// A valid buffer with arbitrary bytes overwritten must never panic on + /// deserialization; any result is acceptable. + #[test] + fn fuzz_mutated_valid_buffer( + len in 0usize ..= 300, + muts in prop::collection::vec((any::(), any::()), 0 ..= 16), + ) { + let original: BitVec = BitVec::repeat(false, len); + let mut bytes = bincode::serialize(&original).unwrap(); + for (idx, val) in muts { + if !bytes.is_empty() { + let i = idx % bytes.len(); + bytes[i] = val; + } + } + let _ = deser(&bytes); + } + + /// Arbitrary input must never panic on deserialization; any result is + /// acceptable. + #[test] + fn fuzz_arbitrary_bytes(bytes in prop::collection::vec(any::(), 0 ..= 512)) { + let _ = deser(&bytes); + } +}