From 582ea1b1009534561c5afcab48da2bbcb7ca0936 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 8 May 2026 17:21:33 +0100 Subject: [PATCH] Pre-check json.dumps indent string size against resource limits The integer indent= argument was converted directly into a repeated space string on the native heap with no resource pre-check, so a large value would allocate well past the configured memory budget. Validate the requested count against the resource tracker before materializing the indent string, raising MemoryError instead. Other .repeat() call sites already carry the same guard. --- crates/monty/src/modules/json/dump.rs | 23 ++++++++++++------ crates/monty/tests/resource_limits.rs | 34 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/crates/monty/src/modules/json/dump.rs b/crates/monty/src/modules/json/dump.rs index e949a13bf..2ba2c5008 100644 --- a/crates/monty/src/modules/json/dump.rs +++ b/crates/monty/src/modules/json/dump.rs @@ -15,7 +15,7 @@ use crate::{ exception_private::{ExcType, RunResult}, heap::{DropWithHeap, HeapData, HeapGuard, HeapId, HeapReadOutput}, intern::StaticStrings, - resource::ResourceTracker, + resource::{ResourceTracker, check_repeat_size}, sorting::{apply_permutation, sort_indices}, types::{PyTrait, long_int::check_bigint_str_digits_limit, str::allocate_string}, value::Value, @@ -251,13 +251,14 @@ fn parse_indent_value(value: Value, vm: &mut VM<'_, impl ResourceTracker>) -> Ru match value { Value::None => Ok(None), Value::Bool(flag) => Ok(Some(" ".repeat(usize::from(*flag)))), - Value::Int(count) => spaces_from_indent_count(*count), + Value::Int(count) => spaces_from_indent_count(*count, vm.heap.tracker()), Value::InternString(string_id) => Ok(Some(vm.interns.get_str(*string_id).to_owned())), Value::Ref(heap_id) => match vm.heap.read(*heap_id) { HeapReadOutput::Str(string) => Ok(Some(string.get(vm.heap).as_str().to_owned())), - HeapReadOutput::LongInt(long_int) => { - spaces_from_indent_count(long_int.get(vm.heap).to_i64().ok_or_else(ExcType::overflow_c_ssize_t)?) - } + HeapReadOutput::LongInt(long_int) => spaces_from_indent_count( + long_int.get(vm.heap).to_i64().ok_or_else(ExcType::overflow_c_ssize_t)?, + vm.heap.tracker(), + ), _ => Err(ExcType::type_error("indent must be None, an integer or a string")), }, _ => Err(ExcType::type_error("indent must be None, an integer or a string")), @@ -268,12 +269,20 @@ fn parse_indent_value(value: Value, vm: &mut VM<'_, impl ResourceTracker>) -> Ru /// /// Zero and negative values return an empty indent string, which keeps pretty /// printing enabled while omitting leading spaces on each line like CPython. -fn spaces_from_indent_count(count: i64) -> RunResult> { +/// +/// The requested count is validated against the resource tracker before the +/// repeat is materialized: the indent string is built on the native heap and +/// then repeated once per nesting level by `write_indent`, so an unbounded +/// count would otherwise allocate well past the configured memory limit. +fn spaces_from_indent_count(count: i64, tracker: &impl ResourceTracker) -> RunResult> { if count <= 0 { Ok(Some(String::new())) } else { match usize::try_from(count) { - Ok(count) => Ok(Some(" ".repeat(count))), + Ok(count) => { + check_repeat_size(count, 1, tracker)?; + Ok(Some(" ".repeat(count))) + } Err(_) => Err(ExcType::overflow_c_ssize_t()), } } diff --git a/crates/monty/tests/resource_limits.rs b/crates/monty/tests/resource_limits.rs index d96243817..d8f293624 100644 --- a/crates/monty/tests/resource_limits.rs +++ b/crates/monty/tests/resource_limits.rs @@ -2039,3 +2039,37 @@ len(x) + len(d) + len(s) ); assert_eq!(result.unwrap(), MontyObject::Int(300)); } + +// === json.dumps resource-limit tests === + +/// Test that `json.dumps(..., indent=N)` is rejected before allocation when the +/// requested indent string would exceed the configured memory limit. +#[test] +fn json_dumps_indent_memory_limit() { + let code = "import json\njson.dumps([1], indent=1000000)"; + let ex = MontyRun::new(code.to_owned(), "test.py", vec![]).unwrap(); + + let limits = ResourceLimits::new().max_memory(100_000); + let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout); + + assert!(result.is_err(), "large indent should be rejected before allocation"); + let exc = result.unwrap_err(); + assert_eq!(exc.exc_type(), ExcType::MemoryError); + assert!( + exc.message().is_some_and(|m| m.contains("memory limit exceeded")), + "expected memory limit error, got: {exc}" + ); +} + +/// Test that small `json.dumps(..., indent=N)` works within limits. +#[test] +fn json_dumps_indent_within_limit() { + let code = "import json\njson.dumps([1, 2], indent=2)"; + let ex = MontyRun::new(code.to_owned(), "test.py", vec![]).unwrap(); + + let limits = ResourceLimits::new().max_memory(100_000); + let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout); + + assert!(result.is_ok(), "small indent should succeed"); + assert_eq!(result.unwrap(), MontyObject::String("[\n 1,\n 2\n]".to_owned())); +}