Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions rust/fory-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ pub struct Config {
/// When enabled, shared references and circular references are tracked
/// and preserved during serialization/deserialization.
pub track_ref: bool,
/// Maximum byte length of a single deserialized binary payload.
pub max_binary_size: usize,
/// Maximum element count of a single deserialized collection or map.
pub max_collection_size: usize,
}

impl Default for Config {
Expand All @@ -50,6 +54,8 @@ impl Default for Config {
max_dyn_depth: 5,
check_struct_version: false,
track_ref: false,
max_binary_size: 64 * 1024 * 1024,
max_collection_size: 1_000_000,
}
}
}
Expand Down Expand Up @@ -101,4 +107,14 @@ impl Config {
pub fn is_track_ref(&self) -> bool {
self.track_ref
}

#[inline(always)]
pub fn max_binary_size(&self) -> usize {
self.max_binary_size
}

#[inline(always)]
pub fn max_collection_size(&self) -> usize {
self.max_collection_size
}
}
28 changes: 28 additions & 0 deletions rust/fory-core/src/fory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,34 @@ impl Fory {
self
}

/// Sets the maximum byte length of a single deserialized binary payload.
///
/// # Examples
///
/// ```rust
/// use fory_core::Fory;
///
/// let fory = Fory::default().max_binary_size(64 * 1024 * 1024);
/// ```
pub fn max_binary_size(mut self, max: usize) -> Self {
self.config.max_binary_size = max;
self
}

/// Sets the maximum element count of a single deserialized collection or map.
///
/// # Examples
///
/// ```rust
/// use fory_core::Fory;
///
/// let fory = Fory::default().max_collection_size(10_000);
/// ```
pub fn max_collection_size(mut self, max: usize) -> Self {
self.config.max_collection_size = max;
self
}

/// Returns whether cross-language serialization is enabled.
pub fn is_xlang(&self) -> bool {
self.config.xlang
Expand Down
13 changes: 13 additions & 0 deletions rust/fory-core/src/resolver/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ pub struct ReadContext<'a> {
xlang: bool,
max_dyn_depth: u32,
check_struct_version: bool,
max_collection_size: usize,

// Context-specific fields
pub reader: Reader<'a>,
Expand Down Expand Up @@ -342,6 +343,7 @@ impl<'a> ReadContext<'a> {
xlang: config.xlang,
max_dyn_depth: config.max_dyn_depth,
check_struct_version: config.check_struct_version,
max_collection_size: config.max_collection_size,
reader: Reader::default(),
meta_resolver: MetaReaderResolver::default(),
meta_string_resolver: MetaStringReaderResolver::default(),
Expand Down Expand Up @@ -472,6 +474,17 @@ impl<'a> ReadContext<'a> {
self.meta_string_resolver.read_meta_string(&mut self.reader)
}

#[inline(always)]
pub fn check_collection_size(&self, len: usize) -> Result<(), Error> {
if len > self.max_collection_size {
return Err(Error::invalid_data(format!(
"collection length {} exceeds configured limit {}",
len, self.max_collection_size
)));
}
Ok(())
}

#[inline(always)]
pub fn inc_depth(&mut self) -> Result<(), Error> {
self.current_depth += 1;
Expand Down
24 changes: 24 additions & 0 deletions rust/fory-core/src/serializer/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ where
if len == 0 {
return Ok(C::from_iter(std::iter::empty()));
}
let remaining = context
.reader
.bf
.len()
.saturating_sub(context.reader.cursor);
if len as usize > remaining {
return Err(Error::invalid_data(format!(
"collection length {} exceeds buffer remaining {}",
len, remaining
)));
}
context.check_collection_size(len as usize)?;
if T::fory_is_polymorphic() || T::fory_is_shared_ref() {
return read_collection_data_dyn_ref(context, len);
}
Expand Down Expand Up @@ -271,6 +283,18 @@ where
if len == 0 {
return Ok(Vec::new());
}
let remaining = context
.reader
.bf
.len()
.saturating_sub(context.reader.cursor);
if len as usize > remaining {
return Err(Error::invalid_data(format!(
"collection length {} exceeds buffer remaining {}",
len, remaining
)));
}
context.check_collection_size(len as usize)?;
if T::fory_is_polymorphic() || T::fory_is_shared_ref() {
return read_vec_data_dyn_ref(context, len);
}
Expand Down
36 changes: 30 additions & 6 deletions rust/fory-core/src/serializer/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,18 +547,30 @@ impl<K: Serializer + ForyDefault + Eq + std::hash::Hash, V: Serializer + ForyDef

fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
let len = context.reader.read_varuint32()?;
let mut map = HashMap::<K, V>::with_capacity(len as usize);
if len == 0 {
return Ok(map);
return Ok(HashMap::new());
}
let remaining = context
.reader
.bf
.len()
.saturating_sub(context.reader.cursor);
if len as usize > remaining {
return Err(Error::invalid_data(format!(
"map entry count {} exceeds buffer remaining {}",
len, remaining
)));
}
context.check_collection_size(len as usize)?;
if K::fory_is_polymorphic()
|| K::fory_is_shared_ref()
|| V::fory_is_polymorphic()
|| V::fory_is_shared_ref()
{
let map: HashMap<K, V> = HashMap::with_capacity(len as usize);
let map = HashMap::<K, V>::with_capacity(len as usize);
return read_hashmap_data_dyn_ref(context, map, len);
}
let mut map = HashMap::<K, V>::with_capacity(len as usize);
let mut len_counter = 0;
loop {
if len_counter == len {
Expand Down Expand Up @@ -698,18 +710,30 @@ impl<K: Serializer + ForyDefault + Ord + std::hash::Hash, V: Serializer + ForyDe

fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
let len = context.reader.read_varuint32()?;
let mut map = BTreeMap::<K, V>::new();
if len == 0 {
return Ok(map);
return Ok(BTreeMap::new());
}
let remaining = context
.reader
.bf
.len()
.saturating_sub(context.reader.cursor);
if len as usize > remaining {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work for streaming deserialization

return Err(Error::invalid_data(format!(
"map entry count {} exceeds buffer remaining {}",
len, remaining
)));
}
context.check_collection_size(len as usize)?;
if K::fory_is_polymorphic()
|| K::fory_is_shared_ref()
|| V::fory_is_polymorphic()
|| V::fory_is_shared_ref()
{
let map: BTreeMap<K, V> = BTreeMap::new();
let map = BTreeMap::<K, V>::new();
return read_btreemap_data_dyn_ref(context, map, len);
}
let mut map = BTreeMap::<K, V>::new();
let mut len_counter = 0;
loop {
if len_counter == len {
Expand Down
16 changes: 16 additions & 0 deletions rust/fory-core/src/serializer/primitive_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,23 @@ pub fn fory_read_data<T: Serializer>(context: &mut ReadContext) -> Result<Vec<T>
if size_bytes % std::mem::size_of::<T>() != 0 {
return Err(Error::invalid_data("Invalid data length"));
}
// Check that the declared byte length is actually available in the buffer
// BEFORE allocating. Without this, a crafted size_bytes can trigger an OOM
// allocation (Vec::with_capacity) even though read_bytes would later reject
// the payload — but only after the damage is done.
let remaining = context
.reader
.bf
.len()
.saturating_sub(context.reader.cursor);
if size_bytes > remaining {
return Err(Error::invalid_data(format!(
"primitive list byte length {} exceeds buffer remaining {}",
size_bytes, remaining
)));
}
let len = size_bytes / std::mem::size_of::<T>();
context.check_collection_size(len)?;
let mut vec: Vec<T> = Vec::with_capacity(len);

#[cfg(target_endian = "little")]
Expand Down
1 change: 1 addition & 0 deletions rust/tests/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ mod compatible;
mod test_any;
mod test_collection;
mod test_max_dyn_depth;
mod test_size_guardrails;
mod test_tuple;
Loading
Loading