Skip to content
Open
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
9 changes: 9 additions & 0 deletions rust/fory-core/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,9 @@ impl<'a> Reader<'a> {

// ============ STRING (TypeId = 19) ============

/// # Caller Contract
/// Validate `len` against `ReadContext::check_string_bytes` before calling this.
/// `check_bound` only verifies buffer has `len` bytes; it does not enforce size limits.
#[inline(always)]
pub fn read_latin1_string(&mut self, len: usize) -> Result<String, Error> {
self.check_bound(len)?;
Expand Down Expand Up @@ -913,6 +916,9 @@ impl<'a> Reader<'a> {
}
}

/// # Caller Contract
/// Validate `len` against `ReadContext::check_string_bytes` before calling this.
/// `check_bound` only verifies buffer has `len` bytes; it does not enforce size limits.
#[inline(always)]
pub fn read_utf8_string(&mut self, len: usize) -> Result<String, Error> {
self.check_bound(len)?;
Expand All @@ -930,6 +936,9 @@ impl<'a> Reader<'a> {
}
}

/// # Caller Contract
/// Validate `len` against `ReadContext::check_string_bytes` before calling this.
/// `check_bound` only verifies buffer has `len` bytes; it does not enforce size limits.
#[inline(always)]
pub fn read_utf16_string(&mut self, len: usize) -> Result<String, Error> {
self.check_bound(len)?;
Expand Down
24 changes: 24 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 byte length of a single deserialized string. `None` = unlimited.
pub max_string_bytes: Option<usize>,
/// Maximum element count of a single deserialized collection. `None` = unlimited.
pub max_collection_size: Option<usize>,
/// Maximum entry count of a single deserialized map. `None` = unlimited.
pub max_map_size: Option<usize>,
}

impl Default for Config {
Expand All @@ -50,6 +56,9 @@ impl Default for Config {
max_dyn_depth: 5,
check_struct_version: false,
track_ref: false,
max_string_bytes: None,
max_collection_size: None,
max_map_size: None,
}
}
}
Expand Down Expand Up @@ -101,4 +110,19 @@ impl Config {
pub fn is_track_ref(&self) -> bool {
self.track_ref
}

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

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

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

/// Sets the maximum byte length of a single deserialized string. Default is no limit.
///
/// # Examples
///
/// ```rust
/// use fory_core::Fory;
///
/// let fory = Fory::default().max_string_bytes(1024 * 1024);
/// ```
pub fn max_string_bytes(mut self, max: usize) -> Self {
self.config.max_string_bytes = Some(max);
self
}

/// Sets the maximum element count of a single deserialized collection. Default is no limit.
///
/// # 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 = Some(max);
self
}

/// Sets the maximum entry count of a single deserialized map. Default is no limit.
///
/// # Examples
///
/// ```rust
/// use fory_core::Fory;
///
/// let fory = Fory::default().max_map_size(10_000);
/// ```
pub fn max_map_size(mut self, max: usize) -> Self {
self.config.max_map_size = Some(max);
self
}

/// Returns whether cross-language serialization is enabled.
pub fn is_xlang(&self) -> bool {
self.config.xlang
Expand Down
45 changes: 45 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,9 @@ pub struct ReadContext<'a> {
xlang: bool,
max_dyn_depth: u32,
check_struct_version: bool,
max_string_bytes: Option<usize>,
max_collection_size: Option<usize>,
max_map_size: Option<usize>,

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

#[inline(always)]
pub fn check_string_bytes(&self, len: usize) -> Result<(), Error> {
if let Some(max) = self.max_string_bytes {
if len > max {
return Err(Error::invalid_data(format!(
"string byte length {} exceeds limit {}",
len, max
)));
}
}
Ok(())
}

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

#[inline(always)]
pub fn check_map_size(&self, len: usize) -> Result<(), Error> {
if let Some(max) = self.max_map_size {
if len > max {
return Err(Error::invalid_data(format!(
"map entry count {} exceeds limit {}",
len, max
)));
}
}
Ok(())
}

#[inline(always)]
pub fn inc_depth(&mut self) -> Result<(), Error> {
self.current_depth += 1;
Expand Down
5 changes: 5 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,10 @@ where
if len == 0 {
return Ok(C::from_iter(std::iter::empty()));
}
// Guard must come before the polymorphic dispatch so that non-Vec collections
// (HashSet, LinkedList, BTreeSet, ...) with polymorphic element types are also
// protected. Mirrors the ordering in read_vec_data.
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 +275,7 @@ where
if len == 0 {
return Ok(Vec::new());
}
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
6 changes: 4 additions & 2 deletions rust/fory-core/src/serializer/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ 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()?;
context.check_map_size(len as usize)?;
let mut map = HashMap::<K, V>::with_capacity(len as usize);
if len == 0 {
return Ok(map);
Expand Down Expand Up @@ -698,10 +699,11 @@ 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());
}
context.check_map_size(len as usize)?;
let mut map = BTreeMap::<K, V>::new();
if K::fory_is_polymorphic()
|| K::fory_is_shared_ref()
|| V::fory_is_polymorphic()
Expand Down
1 change: 1 addition & 0 deletions rust/fory-core/src/serializer/primitive_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub fn fory_read_data<T: Serializer>(context: &mut ReadContext) -> Result<Vec<T>
return Err(Error::invalid_data("Invalid data length"));
}
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
9 changes: 9 additions & 0 deletions rust/fory-core/src/serializer/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ impl Serializer for String {
let bitor = context.reader.read_varuint36small()?;
let len = bitor >> 2;
let encoding = bitor & 0b11;
// For UTF-16, `len` is the number of code-units (each 2 bytes wide).
// Convert to a byte budget before checking so the limit means bytes,
// not code-units. Latin-1 and UTF-8 already store `len` as byte count.
let byte_len = if encoding == 1 {
(len as usize).saturating_mul(2)
} else {
len as usize
};
context.check_string_bytes(byte_len)?;
let s = match encoding {
0 => context.reader.read_latin1_string(len as usize),
1 => context.reader.read_utf16_string(len as usize),
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;
118 changes: 118 additions & 0 deletions rust/tests/tests/test_size_guardrails.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use fory_core::fory::Fory;
use std::collections::HashMap;

#[test]
fn test_collection_size_limit_exceeded() {
if fory_core::error::should_panic_on_error() {
return;
}
let fory_write = Fory::default();
let items: Vec<i32> = vec![1, 2, 3, 4, 5];
let bytes = fory_write.serialize(&items).unwrap();

let fory_read = Fory::default().max_collection_size(3);
let result: Result<Vec<i32>, _> = fory_read.deserialize(&bytes);
assert!(
result.is_err(),
"Expected deserialization to fail due to collection size limit"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("collection length"));
}

#[test]
fn test_collection_size_within_limit() {
if fory_core::error::should_panic_on_error() {
return;
}
let fory = Fory::default().max_collection_size(5);
let items: Vec<i32> = vec![1, 2, 3, 4, 5];
let bytes = fory.serialize(&items).unwrap();
let result: Result<Vec<i32>, _> = fory.deserialize(&bytes);
assert!(result.is_ok());
}

#[test]
fn test_map_size_limit_exceeded() {
if fory_core::error::should_panic_on_error() {
return;
}
let fory_write = Fory::default();
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("a".to_string(), 1);
map.insert("b".to_string(), 2);
map.insert("c".to_string(), 3);
let bytes = fory_write.serialize(&map).unwrap();

let fory_read = Fory::default().max_map_size(2);
let result: Result<HashMap<String, i32>, _> = fory_read.deserialize(&bytes);
assert!(
result.is_err(),
"Expected deserialization to fail due to map size limit"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("map entry count"));
}

#[test]
fn test_map_size_within_limit() {
if fory_core::error::should_panic_on_error() {
return;
}
let fory = Fory::default().max_map_size(3);
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("a".to_string(), 1);
map.insert("b".to_string(), 2);
map.insert("c".to_string(), 3);
let bytes = fory.serialize(&map).unwrap();
let result: Result<HashMap<String, i32>, _> = fory.deserialize(&bytes);
assert!(result.is_ok());
}

#[test]
fn test_string_size_limit_exceeded() {
if fory_core::error::should_panic_on_error() {
return;
}
let fory_write = Fory::default();
let s = "hello world".to_string();
let bytes = fory_write.serialize(&s).unwrap();

let fory_read = Fory::default().max_string_bytes(5);
let result: Result<String, _> = fory_read.deserialize(&bytes);
assert!(
result.is_err(),
"Expected deserialization to fail due to string size limit"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("string byte length"));
}

#[test]
fn test_string_size_within_limit() {
if fory_core::error::should_panic_on_error() {
return;
}
let fory = Fory::default().max_string_bytes(20);
let s = "hello world".to_string();
let bytes = fory.serialize(&s).unwrap();
let result: Result<String, _> = fory.deserialize(&bytes);
assert!(result.is_ok());
}
Loading