From 46ac849e9758020cfbe51f06de8988593f8e6417 Mon Sep 17 00:00:00 2001 From: ss-es <155648797+ss-es@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:52:14 -0400 Subject: [PATCH 1/7] update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dfe5ae1a..00cebeb3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@
+# This is a fork of [`ferrilab/bitvec`](https://github.com/ferrilab/bitvec/) + # `bitvec` ## A Magnifying Glass for Memory From 35a11f986a841fd9ff4c3095245f0574a30d9c74 Mon Sep 17 00:00:00 2001 From: ss-es <155648797+ss-es@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:56:50 -0400 Subject: [PATCH 2/7] fix panics --- src/serdes/slice.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/serdes/slice.rs b/src/serdes/slice.rs index 1c495ef5..b458c6c0 100644 --- a/src/serdes/slice.rs +++ b/src/serdes/slice.rs @@ -154,6 +154,9 @@ where BitSpan::::new(addr, head, bits) .unwrap_err() })?; + if bits > bv.capacity() { + return Err(BitSpanError::TooLong(bits)); + } bv.set_head(head); bv.set_len(bits); Ok(bv) @@ -214,7 +217,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:?}"))) } } From 7b64196ef22ee8c45dffab8fa212c8bc9fb54ee5 Mon Sep 17 00:00:00 2001 From: sveitser Date: Thu, 4 Jun 2026 11:39:42 +0000 Subject: [PATCH 3/7] test(serde): add deserialize tamper regression and property tests Cover BitVec/BitBox deserialization of tampered bincode buffers where head + bits exceeds the data capacity. The head + bits overflow cases currently abort (set_len assert + Vec::from_raw_parts UB in Drop) and fail until the deserializer validates head + bits against capacity. Adds proptest dev-dependency for the capacity-invariant property test. --- Cargo.toml | 1 + tests/serde_deserialize.rs | 99 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/serde_deserialize.rs 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/tests/serde_deserialize.rs b/tests/serde_deserialize.rs new file mode 100644 index 00000000..0b8d6e19 --- /dev/null +++ b/tests/serde_deserialize.rs @@ -0,0 +1,99 @@ +//! 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 abort the process (`set_len` assertion +//! followed by a `Vec::from_raw_parts` UB precondition violation in `Drop`, +//! exit 134). They are marked `#[ignore]` until the deserializer validates +//! `head + bits` against the buffer length. + +#![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] +#[ignore = "KNOWN BUG: aborts on head + bits overflow; fix pending"] +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] +#[ignore = "KNOWN BUG: aborts on head + bits overflow; fix pending"] +fn head_plus_bits_one_past_capacity_is_rejected() { + assert!(deser(&tampered(2, 32, 2 * BITS_PER_ELEM - 31)).is_err()); +} + +#[test] +#[ignore = "KNOWN BUG: aborts on nonzero head past buffer; fix pending"] +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] + #[ignore = "KNOWN BUG: aborts on head + bits overflow; fix pending"] + 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); + } +} From 694034bb4b0cae43cdb12e08c572b30f3790d83f Mon Sep 17 00:00:00 2001 From: sveitser Date: Thu, 4 Jun 2026 11:46:22 +0000 Subject: [PATCH 4/7] fix(serde): validate head + bits against buffer length on deserialize BitVec/BitBox deserialization checked only bits > capacity (Vec capacity, head ignored), so a tampered head + bits exceeding the live buffer reached set_len and aborted (assert + Vec::from_raw_parts UB in Drop). Validate head + bits <= vec.len() * bits_of::() and return BitSpanError::TooLong otherwise. Un-ignores the four regression/property tests. --- src/serdes/slice.rs | 10 +++++++--- tests/serde_deserialize.rs | 12 ++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/serdes/slice.rs b/src/serdes/slice.rs index b458c6c0..c8fa0270 100644 --- a/src/serdes/slice.rs +++ b/src/serdes/slice.rs @@ -150,13 +150,17 @@ where BitSeqVisitor::, Self, _>::new( |vec, head, bits| unsafe { let addr = vec.as_ptr().into_address(); + let live = vec.len().saturating_mul(bits_of::()); let mut bv = BitVec::try_from_vec(vec).map_err(|_| { BitSpan::::new(addr, head, bits) .unwrap_err() })?; - if bits > bv.capacity() { - return Err(BitSpanError::TooLong(bits)); - } + 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) diff --git a/tests/serde_deserialize.rs b/tests/serde_deserialize.rs index 0b8d6e19..14f148f9 100644 --- a/tests/serde_deserialize.rs +++ b/tests/serde_deserialize.rs @@ -8,10 +8,10 @@ //! Acceptance invariant: a buffer is valid iff `head + bits <= data.len() * //! bits_of::()`. //! -//! The `head + bits` overflow cases abort the process (`set_len` assertion -//! followed by a `Vec::from_raw_parts` UB precondition violation in `Drop`, -//! exit 134). They are marked `#[ignore]` until the deserializer validates -//! `head + bits` against the buffer length. +//! 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"))] @@ -62,20 +62,17 @@ fn head_plus_bits_within_capacity_roundtrips() { } #[test] -#[ignore = "KNOWN BUG: aborts on head + bits overflow; fix pending"] 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] -#[ignore = "KNOWN BUG: aborts on head + bits overflow; fix pending"] fn head_plus_bits_one_past_capacity_is_rejected() { assert!(deser(&tampered(2, 32, 2 * BITS_PER_ELEM - 31)).is_err()); } #[test] -#[ignore = "KNOWN BUG: aborts on nonzero head past buffer; fix pending"] 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()); @@ -87,7 +84,6 @@ proptest! { /// Deserialization never panics, and accepts a buffer iff `head + bits` /// fits the data capacity. #[test] - #[ignore = "KNOWN BUG: aborts on head + bits overflow; fix pending"] fn deserialize_matches_capacity_invariant( data_len in 0usize ..= 4, head in 0u8 ..= 63, From 45f0409aa2b53c28c41901861de8891b123794c3 Mon Sep 17 00:00:00 2001 From: sveitser Date: Thu, 4 Jun 2026 11:49:18 +0000 Subject: [PATCH 5/7] test(serde): add deserialize fuzz tests for mutated and arbitrary input fuzz_mutated_valid_buffer overwrites random bytes of a valid buffer; fuzz_arbitrary_bytes feeds raw bytes. Both assert deserialization never panics. Verified over 200k cases each. --- tests/serde_deserialize.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/serde_deserialize.rs b/tests/serde_deserialize.rs index 14f148f9..fb4d8ccc 100644 --- a/tests/serde_deserialize.rs +++ b/tests/serde_deserialize.rs @@ -92,4 +92,29 @@ proptest! { 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); + } } From 851beb659f644ee9529a60f09295d6a905a30eff Mon Sep 17 00:00:00 2001 From: sveitser Date: Thu, 4 Jun 2026 12:14:02 +0000 Subject: [PATCH 6/7] fix(serde): avoid unwrap_err panic when try_from_vec fails The error path built BitSpan::new(addr, head, bits).unwrap_err(); when try_from_vec fails (bit-length exceeds REGION_MAX_BITS) but the deserialized bits is small, BitSpan::new returns Ok and unwrap_err panics. Return BitSpanError::TooLong(live) directly, where live is the overflowing length. --- src/serdes/slice.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/serdes/slice.rs b/src/serdes/slice.rs index c8fa0270..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,12 +147,9 @@ where FIELDS, BitSeqVisitor::, Self, _>::new( |vec, head, bits| unsafe { - let addr = vec.as_ptr().into_address(); let live = vec.len().saturating_mul(bits_of::()); - let mut bv = BitVec::try_from_vec(vec).map_err(|_| { - BitSpan::::new(addr, head, bits) - .unwrap_err() - })?; + 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); From 42cfebad9904489dbf9e2acc38df01b49784af0e Mon Sep 17 00:00:00 2001 From: ss-es <155648797+ss-es@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:13:24 -0400 Subject: [PATCH 7/7] Revert "update README" This reverts commit 46ac849e9758020cfbe51f06de8988593f8e6417. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 00cebeb3..dfe5ae1a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@
-# This is a fork of [`ferrilab/bitvec`](https://github.com/ferrilab/bitvec/) - # `bitvec` ## A Magnifying Glass for Memory