Skip to content

Commit 06ba425

Browse files
authored
Fix an OOM in deserialization on malicious input (#222)
* poc of oom * fix the oom
1 parent 2fa5bad commit 06ba425

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

src/tinyvec.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,25 @@ where
234234
}
235235
}
236236

237+
/// Caps an untrusted, deserialized element count before it is handed to
238+
/// `with_capacity`, so a hostile length prefix cannot force a huge eager
239+
/// allocation (and its allocation-abort DoS) before a single element has been
240+
/// read. The reservation is limited to `MAX_PREALLOC_BYTES` worth of items; the
241+
/// container still grows to the real length via `push` as elements actually
242+
/// arrive, so well-formed input is unaffected.
243+
#[cfg(any(feature = "borsh", feature = "bin-proto", feature = "serde"))]
244+
fn cautious_capacity<T>(len: usize) -> usize {
245+
// Mirrors serde's `size_hint::cautious`: never trust a wire-provided length
246+
// as an allocation size.
247+
const MAX_PREALLOC_BYTES: usize = 4096;
248+
let item_size = core::mem::size_of::<T>();
249+
if item_size == 0 {
250+
len
251+
} else {
252+
core::cmp::min(len, MAX_PREALLOC_BYTES / item_size)
253+
}
254+
}
255+
237256
#[cfg(feature = "borsh")]
238257
#[cfg_attr(docs_rs, doc(cfg(feature = "borsh")))]
239258
impl<A: Array> borsh::BorshDeserialize for TinyVec<A>
@@ -244,7 +263,8 @@ where
244263
reader: &mut R,
245264
) -> borsh::io::Result<Self> {
246265
let len = <usize as borsh::BorshDeserialize>::deserialize_reader(reader)?;
247-
let mut new_tinyvec = Self::with_capacity(len);
266+
let mut new_tinyvec =
267+
Self::with_capacity(cautious_capacity::<A::Item>(len));
248268

249269
for _ in 0..len {
250270
new_tinyvec.push(
@@ -313,7 +333,8 @@ where
313333
{
314334
let item_count =
315335
tag.0.try_into().map_err(|_| bin_proto::Error::TagConvert)?;
316-
let mut values = Self::with_capacity(item_count);
336+
let mut values =
337+
Self::with_capacity(cautious_capacity::<A::Item>(item_count));
317338
for _ in 0..item_count {
318339
values.push(bin_proto::BitDecode::<_, _>::decode::<_, E>(read, ctx, ())?);
319340
}
@@ -1951,7 +1972,9 @@ where
19511972
S: SeqAccess<'de>,
19521973
{
19531974
let mut new_tinyvec = match seq.size_hint() {
1954-
Some(expected_size) => TinyVec::with_capacity(expected_size),
1975+
Some(expected_size) => {
1976+
TinyVec::with_capacity(cautious_capacity::<A::Item>(expected_size))
1977+
}
19551978
None => Default::default(),
19561979
};
19571980

tests/tinyvec.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,20 @@ fn TinyVec_borsh_de_heap() {
478478
assert_eq!(tv, des);
479479
}
480480

481+
#[cfg(feature = "borsh")]
482+
#[test]
483+
fn TinyVec_borsh_de_hostile_length_no_abort() {
484+
// A tiny buffer that claims an enormous element count but supplies no
485+
// elements. Before the cautious-capacity fix this drove
486+
// `with_capacity(huge)` and aborted the process; now the eager reservation
487+
// is bounded and decoding simply fails on the missing element bytes.
488+
let huge: u64 = 1u64 << 60;
489+
let mut buffer = Vec::new();
490+
buffer.extend_from_slice(&huge.to_le_bytes());
491+
let des: Result<TinyVec<[u32; 4]>, _> = borsh::from_slice(&buffer);
492+
assert!(des.is_err());
493+
}
494+
481495
#[cfg(feature = "bin-proto")]
482496
#[test]
483497
fn TinyVec_bin_proto_encode_untagged() {

0 commit comments

Comments
 (0)