From de95640d93017d924e94cbe6d5841b276ae94d4a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 08:09:22 -0800 Subject: [PATCH] JavaScript: XML namespace declarations and QName strings count against the heap limit addNamespace and setNamespace now add a declaration's bytes to the heap total when the element gains a new prefix, and QName.prototype.toString adds the bytes of the string it returns. Before, both only checked for room, so a loop of distinct prefixes could grow an element past the heap limit until a collection recounted it. --- crates/js/src/xml_builtins.rs | 10 ++++++++-- crates/js/src/xml_tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/js/src/xml_builtins.rs b/crates/js/src/xml_builtins.rs index 1662dc5db..35c578830 100644 --- a/crates/js/src/xml_builtins.rs +++ b/crates/js/src/xml_builtins.rs @@ -299,7 +299,9 @@ fn qname_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> Js _ => {} } units.extend_from_slice(&name.local); - interp.reserve_heap(string_bytes(units.len()))?; + let bytes = string_bytes(units.len()); + interp.reserve_heap(bytes)?; + interp.heap.bytes = interp.heap.bytes.saturating_add(bytes); Ok(Value::String(Str::from_units(units))) } @@ -816,7 +818,11 @@ fn add_in_scope(interp: &mut Interp, x: ObjId, ns: XmlNs) -> JsResult<()> { if prefix.is_empty() && node.name.as_ref().and_then(|n| n.uri.as_ref()).is_some_and(|u| u.is_empty()) { return Ok(()); } - interp.reserve_heap(crate::heap::XML_NS_BYTES)?; + if !node.ns.iter().any(|m| m.prefix.as_ref() == Some(&prefix)) { + let bytes = crate::heap::XML_NS_BYTES; + interp.reserve_heap(bytes)?; + interp.heap.bytes = interp.heap.bytes.saturating_add(bytes); + } let attrs = interp.xml_node(x)?.attrs.clone(); let node = interp.xml_node_mut(x)?; node.ns.retain(|m| m.prefix.as_ref() != Some(&prefix)); diff --git a/crates/js/src/xml_tests.rs b/crates/js/src/xml_tests.rs index 3d801e050..051c676a9 100644 --- a/crates/js/src/xml_tests.rs +++ b/crates/js/src/xml_tests.rs @@ -1018,6 +1018,32 @@ fn comparing_xml_values_is_charged_to_the_heap() { assert!(heap_stops_only(setup, "(SPARE, x == y)", "x == y && y.@q == '2'") > 0); } +/// Runs `setup` with no limit, then `action` in the same realm, and returns how `action` ended. +fn after_setup(setup: &str, action: &str) -> Result { + let mut r = realm(); + let interp = r.interp(); + let global = interp.global; + assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); + r.run(setup).unwrap(); + r.run(action).map_err(|e| e.kind) +} + +#[test] +fn namespace_declarations_and_qname_strings_are_charged_to_the_heap() { + let heap = Err(RunErrorKind::Limit(LimitKind::Heap)); + let distinct = "var x = ; var a = []; for (var i = 0; i < 400; i++) a.push(new Namespace('p' + i, 'u' + i));"; + for method in ["addNamespace", "setNamespace"] { + let action = format!("squeeze(4096); for (var i = 0; i < 400; i++) x.{method}(a[i]); 1"); + assert_eq!(after_setup(distinct, &action), heap, "{method}"); + } + let same = "var x = ; var n = new Namespace('p', 'u');"; + let action = "squeeze(4096); for (var i = 0; i < 400; i++) x.addNamespace(n); 1"; + assert_eq!(after_setup(same, action), Ok(JsValue::Number(1.0))); + let long = "var s = 'x'; for (var i = 0; i < 12; i++) s += s; var q = new QName('u', s);"; + let action = "squeeze(12000); var t1 = q.toString(); var t2 = q.toString(); 1"; + assert_eq!(after_setup(long, action), heap); +} + #[test] fn an_interrupt_stops_serialization() { let mut r = realm();