primitives: Apply the witness item size limit to every element
What changed, and why it matters
This commit fixes a bug in how the Rust Bitcoin library checks the size of individual pieces of data inside a Bitcoin transaction witness. Previously, only the first witness element had its size properly capped; after the first element, the size checker was accidentally reset to a much looser default, allowing oversized elements to slip through. The patch makes sure every single witness element gets the same strict size limit.
Review whether any released versions shipped with this behavior and assess if a security advisory is warranted for denial-of-service or consensus-edge cases. Backport the fix to maintained branches.
Security signals we found
Bypass of a protocol-enforced size limit
Resource consumption / denial-of-service vector via oversized witness elements
Inconsistent validation between first and subsequent elements
Regression test added for the bypass scenario
Evidence from the diff
In primitives/src/witness.rs, the WitnessDecoder previously used core::mem::take to move the element_length_decoder after decoding a length prefix. Because CompactSizeDecoder’s Default implementation does not preserve the custom MAX_WITNESS_ITEM_SIZE limit, the replacement decoder had only the default 32 MB compact-size limit. As a result, the per-element 4,000,000-byte witness item size limit was enforced only on the first element. The fix uses core::mem::replace to install a fresh CompactSizeDecoder::new_with_limit(MAX_WITNESS_ITEM_SIZE) each time, so every witness element is subject to the limit. A regression test confirms both the first and a later oversized element are now rejected.
Changed components
primitives/src/witness.rsWitnessDecoderCompactSizeDecoder element length decodingInspect captured patch +26 / −1
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index 3fe1244b..e7e83ab0 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -418,7 +418,10 @@ impl encoding::Decoder for WitnessDecoder {
}
// Take ownership of the decoder so we can consume it.
- let decoder = core::mem::take(&mut self.element_length_decoder);
+ let decoder = core::mem::replace(
+ &mut self.element_length_decoder,
+ CompactSizeDecoder::new_with_limit(MAX_WITNESS_ITEM_SIZE),
+ );
let element_length = decoder.end().map_err(|e| E(Inner::LengthPrefixDecode(e)))?;
// keep the element length prefix in the content area.
@@ -1851,4 +1854,26 @@ mod test {
assert_eq!(dec.content.len(), 5);
assert!(dec.content.capacity() < 100_000);
}
+
+ #[cfg(feature = "alloc")]
+ #[test]
+ fn element_length_limit_applies_to_every_element() {
+ // 4_000_001, which exceeds `MAX_WITNESS_ITEM_SIZE` but is below the default 32MB compact
+ // size limit. It is therefore only rejected if each element gets a correctly limited
+ // decoder, rather than the first one alone.
+ const OVERSIZED: [u8; 5] = [0xFE, 0x01, 0x09, 0x3D, 0x00];
+
+ // As the first element.
+ let mut encoded = vec![0x01];
+ encoded.extend_from_slice(&OVERSIZED);
+ let mut dec = WitnessDecoder::new();
+ assert!(dec.push_bytes(&mut encoded.as_slice()).is_err());
+
+ // And as a later element, which used to be accepted because the length decoder was
+ // replaced with an unlimited-by-comparison default one after the first element.
+ let mut encoded = vec![0x02, 0x00];
+ encoded.extend_from_slice(&OVERSIZED);
+ let mut dec = WitnessDecoder::new();
+ assert!(dec.push_bytes(&mut encoded.as_slice()).is_err());
+ }
}
Why this scored 60/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.