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