Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions rust/fory-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub struct Config {
/// When enabled, shared references and circular references are tracked
/// and preserved during serialization/deserialization.
pub track_ref: bool,
/// Maximum allowed size for binary data in bytes.
/// Prevents excessive memory allocation from untrusted payloads.
pub max_binary_size: u32,
/// Maximum allowed number of elements in a collection or entries in a map.
/// Prevents excessive memory allocation from untrusted payloads.
pub max_collection_size: u32,
}

impl Default for Config {
Expand All @@ -50,6 +56,8 @@ impl Default for Config {
max_dyn_depth: 5,
check_struct_version: false,
track_ref: false,
max_binary_size: 64 * 1024 * 1024, // 64MB default
max_collection_size: 1024 * 1024, // 1M elements default
}
}
}
Expand Down Expand Up @@ -101,4 +109,16 @@ impl Config {
pub fn is_track_ref(&self) -> bool {
self.track_ref
}

/// Get maximum allowed binary data size in bytes.
#[inline(always)]
pub fn max_binary_size(&self) -> u32 {
self.max_binary_size
}

/// Get maximum allowed collection/map element count.
#[inline(always)]
pub fn max_collection_size(&self) -> u32 {
self.max_collection_size
}
}
30 changes: 30 additions & 0 deletions rust/fory-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ pub enum Error {
/// Do not construct this variant directly; use [`Error::struct_version_mismatch`] instead.
#[error("{0}")]
StructVersionMismatch(Cow<'static, str>),

/// Deserialization size limit exceeded.
///
/// Returned when a payload-driven length exceeds a configured guardrail
/// (e.g. `max_binary_size` or `max_collection_size`).
///
/// Do not construct this variant directly; use [`Error::size_limit_exceeded`] instead.
#[error("{0}")]
SizeLimitExceeded(Cow<'static, str>),
}

impl Error {
Expand Down Expand Up @@ -495,6 +504,27 @@ impl Error {
err
}

/// Creates a new [`Error::SizeLimitExceeded`] from a string or static message.
///
/// If `FORY_PANIC_ON_ERROR` environment variable is set, this will panic with the error message.
///
/// # Example
/// ```
/// use fory_core::error::Error;
///
/// let err = Error::size_limit_exceeded("Collection size 2000000 exceeds limit 1048576");
/// ```
#[inline(always)]
#[cold]
#[track_caller]
pub fn size_limit_exceeded<S: Into<Cow<'static, str>>>(s: S) -> Self {
let err = Error::SizeLimitExceeded(s.into());
if PANIC_ON_ERROR {
panic!("FORY_PANIC_ON_ERROR: {}", err);
}
err
}

/// Enhances a [`Error::TypeError`] with additional type name information.
///
/// If the error is a `TypeError`, appends the type name to the message.
Expand Down
81 changes: 77 additions & 4 deletions rust/fory-core/src/fory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,65 @@ impl Fory {
self
}

/// Sets the maximum allowed size for binary data during deserialization.
///
/// # Arguments
///
/// * `max_binary_size` - The maximum number of bytes allowed for a single binary/primitive-array
/// payload during deserialization. Payloads exceeding this limit will cause a
/// `SizeLimitExceeded` error.
///
/// # Returns
///
/// Returns `self` for method chaining.
///
/// # Default
///
/// The default value is `64 * 1024 * 1024` (64 MB).
///
/// # Examples
///
/// ```rust
/// use fory_core::Fory;
///
/// // Limit binary payloads to 1 MB
/// let fory = Fory::default().max_binary_size(1024 * 1024);
/// ```
pub fn max_binary_size(mut self, max_binary_size: u32) -> Self {
self.config.max_binary_size = max_binary_size;
self
}

/// Sets the maximum allowed number of elements in a collection or entries in a map
/// during deserialization.
///
/// # Arguments
///
/// * `max_collection_size` - The maximum number of elements/entries allowed for a single
/// collection or map during deserialization. Payloads exceeding this limit will cause a
/// `SizeLimitExceeded` error.
///
/// # Returns
///
/// Returns `self` for method chaining.
///
/// # Default
///
/// The default value is `1024 * 1024` (1 million elements).
///
/// # Examples
///
/// ```rust
/// use fory_core::Fory;
///
/// // Limit collections to 10000 elements
/// let fory = Fory::default().max_collection_size(10000);
/// ```
pub fn max_collection_size(mut self, max_collection_size: u32) -> Self {
self.config.max_collection_size = max_collection_size;
self
}

/// Returns whether cross-language serialization is enabled.
pub fn is_xlang(&self) -> bool {
self.config.xlang
Expand Down Expand Up @@ -358,6 +417,16 @@ impl Fory {
self.config.max_dyn_depth
}

/// Returns the maximum allowed binary data size in bytes.
pub fn get_max_binary_size(&self) -> u32 {
self.config.max_binary_size
}

/// Returns the maximum allowed collection/map element count.
pub fn get_max_collection_size(&self) -> u32 {
self.config.max_collection_size
}

/// Returns whether class version checking is enabled.
///
/// # Returns
Expand Down Expand Up @@ -599,12 +668,14 @@ impl Fory {
WRITE_CONTEXTS.with(|cache| {
let cache = unsafe { &mut *cache.get() };
let id = self.id;
let config = self.config.clone();

let context = cache.get_or_insert_result(id, || {
// Only fetch type resolver when creating a new context
let type_resolver = self.get_final_type_resolver()?;
Ok(Box::new(WriteContext::new(type_resolver.clone(), config)))
Ok(Box::new(WriteContext::new(
type_resolver.clone(),
self.config.clone(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why you clone config?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

previously it was cloned as well, i just moved it inside creation closure, which avoided its access on every serialize call and using its cached context instead.

)))
})?;
f(context)
})
Expand Down Expand Up @@ -1015,12 +1086,14 @@ impl Fory {
READ_CONTEXTS.with(|cache| {
let cache = unsafe { &mut *cache.get() };
let id = self.id;
let config = self.config.clone();

let context = cache.get_or_insert_result(id, || {
// Only fetch type resolver when creating a new context
let type_resolver = self.get_final_type_resolver()?;
Ok(Box::new(ReadContext::new(type_resolver.clone(), config)))
Ok(Box::new(ReadContext::new(
type_resolver.clone(),
self.config.clone(),
)))
})?;
f(context)
})
Expand Down
16 changes: 16 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,8 @@ pub struct ReadContext<'a> {
xlang: bool,
max_dyn_depth: u32,
check_struct_version: bool,
max_binary_size: u32,
max_collection_size: u32,

// Context-specific fields
pub reader: Reader<'a>,
Expand Down Expand Up @@ -342,6 +344,8 @@ impl<'a> ReadContext<'a> {
xlang: config.xlang,
max_dyn_depth: config.max_dyn_depth,
check_struct_version: config.check_struct_version,
max_binary_size: config.max_binary_size,
max_collection_size: config.max_collection_size,
reader: Reader::default(),
meta_resolver: MetaReaderResolver::default(),
meta_string_resolver: MetaStringReaderResolver::default(),
Expand Down Expand Up @@ -386,6 +390,18 @@ impl<'a> ReadContext<'a> {
self.max_dyn_depth
}

/// Get maximum allowed binary data size in bytes.
#[inline(always)]
pub fn max_binary_size(&self) -> u32 {
self.max_binary_size
}

/// Get maximum allowed collection/map element count.
#[inline(always)]
pub fn max_collection_size(&self) -> u32 {
self.max_collection_size
}

#[inline(always)]
pub fn attach_reader(&mut self, reader: Reader<'a>) {
self.reader = reader;
Expand Down
15 changes: 15 additions & 0 deletions rust/fory-core/src/serializer/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ pub const DECL_ELEMENT_TYPE: u8 = 0b100;
// Whether collection elements type same.
pub const IS_SAME_TYPE: u8 = 0b1000;

#[cold]
fn collection_size_limit_exceeded(len: u32, max: u32) -> Error {
Error::size_limit_exceeded(format!("Collection size {} exceeds limit {}", len, max))
}

fn check_collection_len<T: Serializer>(context: &ReadContext, len: u32) -> Result<(), Error> {
if std::mem::size_of::<T>() == 0 {
return Ok(());
Expand Down Expand Up @@ -95,6 +100,8 @@ where
context.writer.write_u8(header);
T::fory_write_type_info(context)?;
}
// Pre-reserve buffer space to avoid per-element capacity checks in the write loop.
context.writer.reserve(len * T::fory_reserved_space());
if !has_null {
for item in iter {
item.fory_write_data_generic(context, has_generics)?;
Expand Down Expand Up @@ -243,6 +250,10 @@ where
if len == 0 {
return Ok(C::from_iter(std::iter::empty()));
}
let max = context.max_collection_size();
if len > max {
return Err(collection_size_limit_exceeded(len, max));
}
if T::fory_is_polymorphic() || T::fory_is_shared_ref() {
return read_collection_data_dyn_ref(context, len);
}
Expand Down Expand Up @@ -285,6 +296,10 @@ where
if len == 0 {
return Ok(Vec::new());
}
let max = context.max_collection_size();
if len > max {
return Err(collection_size_limit_exceeded(len, max));
}
if T::fory_is_polymorphic() || T::fory_is_shared_ref() {
return read_vec_data_dyn_ref(context, len);
}
Expand Down
13 changes: 13 additions & 0 deletions rust/fory-core/src/serializer/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ const TRACKING_VALUE_REF: u8 = 0b1000;
pub const VALUE_NULL: u8 = 0b10000;
pub const DECL_VALUE_TYPE: u8 = 0b100000;

#[cold]
fn map_size_limit_exceeded(len: u32, max: u32) -> Error {
Error::size_limit_exceeded(format!("Map size {} exceeds limit {}", len, max))
}

fn check_map_len(context: &ReadContext, len: u32) -> Result<(), Error> {
let len = len as usize;
let remaining = context.reader.slice_after_cursor().len();
Expand Down Expand Up @@ -560,6 +565,10 @@ impl<K: Serializer + ForyDefault + Eq + std::hash::Hash, V: Serializer + ForyDef
if len == 0 {
return Ok(HashMap::new());
}
let max = context.max_collection_size();
if len > max {
return Err(map_size_limit_exceeded(len, max));
}
check_map_len(context, len)?;
if K::fory_is_polymorphic()
|| K::fory_is_shared_ref()
Expand Down Expand Up @@ -712,6 +721,10 @@ impl<K: Serializer + ForyDefault + Ord + std::hash::Hash, V: Serializer + ForyDe
if len == 0 {
return Ok(BTreeMap::new());
}
let max = context.max_collection_size();
if len > max {
return Err(map_size_limit_exceeded(len, max));
}
check_map_len(context, len)?;
let mut map = BTreeMap::<K, V>::new();
if K::fory_is_polymorphic()
Expand Down
9 changes: 9 additions & 0 deletions rust/fory-core/src/serializer/primitive_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ use crate::resolver::context::WriteContext;
use crate::serializer::Serializer;
use crate::types::TypeId;

#[cold]
fn binary_size_limit_exceeded(size_bytes: usize, max: usize) -> Error {
Error::size_limit_exceeded(format!("Binary size {} exceeds limit {}", size_bytes, max))
}

pub fn fory_write_data<T: Serializer>(this: &[T], context: &mut WriteContext) -> Result<(), Error> {
// U128, USIZE, ISIZE, INT128 are Rust-specific and not supported in xlang mode
if context.is_xlang() {
Expand Down Expand Up @@ -83,6 +88,10 @@ 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"));
}
let max = context.max_binary_size() as usize;
if size_bytes > max {
return Err(binary_size_limit_exceeded(size_bytes, max));
}
let remaining = context.reader.slice_after_cursor().len();
if size_bytes > remaining {
let cursor = context.reader.get_cursor();
Expand Down
26 changes: 26 additions & 0 deletions rust/tests/tests/test_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,29 @@ fn test_heap_container() {
assert_eq!(deserialized.binary_heap.len(), 3);
assert_eq!(deserialized.binary_heap.peek(), Some(&3));
}

#[test]
fn test_hashset_max_collection_size_guardrail() {
let fory = Fory::default();
let original = HashSet::from([
"apple".to_string(),
"banana".to_string(),
"cherry".to_string(),
]);
let serialized = fory.serialize(&original).unwrap();

let limited_fory = Fory::default().max_collection_size(2);
let err = limited_fory
.deserialize::<HashSet<String>>(&serialized)
.expect_err("expected collection size guardrail to reject the payload");

assert!(
matches!(err, fory_core::Error::SizeLimitExceeded(_)),
"expected SizeLimitExceeded, got: {err}"
);
assert!(
err.to_string()
.contains("Collection size 3 exceeds limit 2"),
"unexpected error message: {err}"
);
}
Loading
Loading