JavaScript: parseInt and parseFloat count their copies toward the heap limit

parseInt collected every digit into a vector outside the heap count and
then copied the digits again for a base-10 parse, and parseFloat copied
its whole decimal prefix the same way, so one call on a long string could
allocate more than the heap limit. parseInt now counts the digits in
place, reads bases other than 10 straight from the string, and both
functions reserve heap for the base-10 copy before they make it. A
concatenation that the global functions flatten is charged to the heap,
as sort keys are.
This commit is contained in:
dotCooCoo
2026-11-07 18:28:44 -08:00
parent 28bc571e34
commit 60e617f861
2 changed files with 62 additions and 10 deletions
+35 -10
View File
@@ -33,13 +33,27 @@ fn trim_start(s: &[u16]) -> &[u16] {
s.get(s.iter().position(|&u| !is_str_space(u)).unwrap_or(s.len())..).unwrap_or_default()
}
/// ToString of the first argument, as code units, with each one taking a unit of fuel.
/// ToString of the first argument, as code units, with each one taking a unit of fuel. A
/// concatenation is flattened into a new buffer, which is charged to the heap.
fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult<std::rc::Rc<[u16]>> {
let s = interp.to_str(&arg(args, 0))?.flat();
let s = interp.to_str(&arg(args, 0))?;
for _ in 0..s.len() {
interp.use_fuel()?;
}
Ok(s)
let rope = s.buffer_addr().is_none();
let bytes = crate::heap::string_bytes(s.len());
if rope {
let mark = interp.temp_roots.len();
interp.temp_roots.push(Value::String(s.clone()));
let reserved = interp.reserve_heap(bytes);
interp.temp_roots.truncate(mark);
reserved?;
}
let units = s.flat();
if rope {
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
}
Ok(units)
}
fn digit_value(u: u16) -> Option<u32> {
@@ -76,19 +90,31 @@ fn parse_int(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult
rest = rest.get(2..).unwrap_or_default();
r = 16;
}
let digits: Vec<u32> = rest.iter().map_while(|&u| digit_value(u).filter(|&d| d < r)).collect();
if digits.is_empty() {
let n = rest.iter().map_while(|&u| digit_value(u).filter(|&d| d < r)).count();
let prefix = rest.get(..n).unwrap_or_default();
if prefix.is_empty() {
return Ok(Value::Number(f64::NAN));
}
let digits = prefix.iter().filter_map(|&u| digit_value(u));
let value = match r {
10 => digits.iter().filter_map(|&d| char::from_digit(d, 10)).collect::<String>().parse::<f64>().unwrap_or(f64::NAN),
2 | 4 | 8 | 16 | 32 => crate::chars::radix_value(digits.iter().copied(), r).unwrap_or(f64::NAN),
10 => parse_decimal(interp, prefix)?,
2 | 4 | 8 | 16 | 32 => crate::chars::radix_value(digits, r).unwrap_or(f64::NAN),
// Other radices may be approximated (section 15.1.2.2, step 13).
_ => digits.iter().fold(0.0, |n, &d| n * f64::from(r) + f64::from(d)),
_ => digits.fold(0.0, |n, d| n * f64::from(r) + f64::from(d)),
};
Ok(Value::Number(sign * value))
}
/// The number that `text`, ASCII decimal digits with an optional fraction and exponent, stands
/// for. The text is copied into a string of one byte per code unit for parsing, and that copy is
/// checked against the heap limit first.
fn parse_decimal(interp: &mut Interp, text: &[u16]) -> JsResult<f64> {
interp.reserve_heap(text.len())?;
let mut ascii = String::with_capacity(text.len());
ascii.extend(text.iter().filter_map(|&u| char::from_u32(u32::from(u))));
Ok(ascii.parse::<f64>().unwrap_or(f64::NAN))
}
/// The length of the longest prefix of `s` that is a StrDecimalLiteral without its sign and
/// without `Infinity`.
fn decimal_prefix(s: &[u16]) -> usize {
@@ -148,8 +174,7 @@ fn parse_float(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResu
if n == 0 {
return Ok(Value::Number(f64::NAN));
}
let text = String::from_utf16_lossy(rest.get(..n).unwrap_or_default());
Ok(Value::Number(sign * text.parse::<f64>().unwrap_or(f64::NAN)))
Ok(Value::Number(sign * parse_decimal(interp, rest.get(..n).unwrap_or_default())?))
}
/// `isNaN` (section 15.1.2.4).
+27
View File
@@ -329,6 +329,33 @@ fn define_properties_checks_the_heap_before_each_new_property() {
assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes);
}
#[test]
fn number_parsing_counts_its_copy_toward_the_heap_limit() {
// `s` is 1 Mi digits, so parsing it in base 10 copies 1 MiB, which does not fit in the
// 512 KiB left. Bases 16 and 36 read the digits where they are and still fit, but
// flattening a concatenation of `s` makes a 2 MiB buffer, which does not.
let setup = "var s = new Array((1 << 20) + 1).join('1'); var pad = new Array(800000).join('2'); 0";
let mut measure = small(Limits::default());
assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0)));
measure.interp().collect_garbage();
let heap_bytes = measure.interp().heap.bytes + (512 << 10);
assert!(heap_bytes >= 4 << 20, "{heap_bytes}");
let limits = Limits { heap_bytes, ..Limits::default() };
let mut realm = small(limits.clone());
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
assert_eq!(
realm.run("parseInt(s, 16) > 0 && parseInt(s, 36) > 0 && parseInt('12') === 12 && parseFloat('1.5') === 1.5"),
Ok(JsValue::Bool(true))
);
assert_eq!(limit(&mut realm, "parseInt(s)"), LimitKind::Heap);
let mut realm = small(limits.clone());
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
assert_eq!(limit(&mut realm, "parseInt(s + '1', 16)"), LimitKind::Heap);
let mut realm = small(limits);
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
assert_eq!(limit(&mut realm, "parseFloat(s)"), LimitKind::Heap);
}
#[test]
fn values_built_ins_hold_survive_collections() {
// A small heap makes the callbacks' garbage start collections while the results are built.