JavaScript: a run clears the last uncaught exception and checks source length before encoding

Realm::run now resets the exception that bind_last_thrown binds, so a run
that completes, reaches a limit or does not parse leaves it undefined. The
script source, the text built by Function and the strings given to eval are
checked against the run's remaining max_source_units before they are encoded
or flattened, so an oversized source is rejected without being copied.
This commit is contained in:
dotCooCoo
2026-11-07 12:45:02 -08:00
parent d026e6ae26
commit ba9e78ebfd
4 changed files with 56 additions and 3 deletions
+2 -1
View File
@@ -221,7 +221,7 @@ fn function_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsRe
}
let body = parts.pop().unwrap_or_default();
let text = format!("(function anonymous({}\n) {{\n{body}\n}})", parts.join(","));
let units: Vec<u16> = text.encode_utf16().collect();
let units = interp.encode_source(&text).map_err(|e| interp.compile_error_to_abrupt(e))?;
interp.source_units = interp.source_units.saturating_add(units.len());
let remaining = interp.options.max_source_units.saturating_sub(interp.source_units.saturating_sub(units.len()));
let options = crate::Options { strict: false, max_source_units: remaining, ..interp.options.clone() };
@@ -264,6 +264,7 @@ fn function_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) ->
fn eval(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
let x = arg(args, 0);
let Value::String(s) = &x else { return Ok(x) };
interp.check_source_units(s.len()).map_err(|e| interp.compile_error_to_abrupt(e))?;
let units = s.flat();
let code = interp.compile_source(&units, false, CodeKind::Eval).map_err(|e| interp.compile_error_to_abrupt(e))?;
interp.run_global(code, true)
+19
View File
@@ -1608,6 +1608,24 @@ impl Interp {
// ---- eval and scripts ----
/// The options for source parsed next in this run: [`Options::max_source_units`] is what
/// the run may still parse.
fn remaining_source_options(&self) -> Options {
Options { max_source_units: self.options.max_source_units.saturating_sub(self.source_units), ..self.options.clone() }
}
/// Checks that `units` more code units of source fit in what this run may still parse,
/// before the source is flattened or encoded.
pub(crate) fn check_source_units(&self, units: usize) -> Result<(), CompileError> {
crate::check_length(units, &self.remaining_source_options()).map_err(CompileError::Syntax)
}
/// Encodes `text` as UTF-16 for parsing, after checking that it fits in what this run may
/// still parse.
pub(crate) fn encode_source(&self, text: &str) -> Result<Vec<u16>, CompileError> {
crate::to_utf16(text, &self.remaining_source_options()).map_err(CompileError::Syntax)
}
/// Parses and compiles source for eval code or a script, counting it against the source
/// limit.
pub(crate) fn compile_source(&mut self, units: &[u16], strict: bool, kind: CodeKind) -> Result<Rc<Code>, CompileError> {
@@ -1637,6 +1655,7 @@ impl Interp {
return self.push(x);
};
let strict = self.strict();
self.check_source_units(s.len()).map_err(|e| self.compile_error_to_abrupt(e))?;
let units = s.flat();
let code = self.compile_source(&units, strict, CodeKind::Eval).map_err(|e| self.compile_error_to_abrupt(e))?;
let frame = self.frame()?;
+33
View File
@@ -372,6 +372,39 @@ fn eval_and_function_source_counts_against_the_source_limit() {
assert!(e.message.contains(crate::TOO_LONG), "{e:?}");
}
#[test]
fn a_script_longer_than_the_source_limit_is_a_syntax_error() {
let mut realm = Realm::with_options(Options { max_source_units: 10, ..Options::default() }, Limits::default());
let e = realm.run("1 + 2 + 3 + 4").unwrap_err();
assert_eq!((&e.kind, e.message.contains(crate::TOO_LONG)), (&RunErrorKind::Syntax, true), "{e:?}");
// Ten UTF-16 code units in 18 bytes of UTF-8 fit.
assert_eq!(realm.run("'\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}'"), Ok(JsValue::String("\u{e9}".repeat(8))));
let mut realm = Realm::with_options(Options { max_source_units: 40, ..Options::default() }, Limits::default());
let e = realm.run("Function('return 1;')").unwrap_err();
assert_eq!((&e.kind, e.message.contains(crate::TOO_LONG)), (&RunErrorKind::Thrown, true), "{e:?}");
}
#[test]
fn bind_last_thrown_binds_undefined_after_a_run_that_did_not_throw() {
let mut realm = Realm::new();
assert_eq!(realm.run("throw 'first'").map_err(|e| e.kind), Err(RunErrorKind::Thrown));
realm.bind_last_thrown("caught");
assert_eq!(realm.run("caught"), Ok(JsValue::String("first".into())));
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)));
realm.bind_last_thrown("caught");
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined));
let _ = realm.run("throw 'second'");
assert_eq!(realm.run("var").map_err(|e| e.kind), Err(RunErrorKind::Syntax));
realm.bind_last_thrown("caught");
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that does not parse throws nothing");
realm.set_limits(Limits { fuel: 100, ..Limits::default() });
let _ = realm.run("throw 'third'");
assert_eq!(limit(&mut realm, "for (;;) {}"), LimitKind::Fuel);
realm.bind_last_thrown("caught");
realm.set_limits(Limits::default());
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that reached a limit threw nothing");
}
#[test]
fn wasm_builds_have_smaller_default_limits() {
let limits = Limits::default();
+2 -2
View File
@@ -173,8 +173,8 @@ impl Realm {
interp.fuel = interp.limits.fuel;
interp.source_units = 0;
interp.throw_site = None;
let units: Vec<u16> = source.encode_utf16().collect();
let code = match interp.compile_source(&units, false, CodeKind::Global) {
interp.last_thrown = Value::Undefined;
let code = match interp.encode_source(source).and_then(|units| interp.compile_source(&units, false, CodeKind::Global)) {
Ok(code) => code,
Err(CompileError::Syntax(e)) => return Err(e.into()),
Err(CompileError::Internal(m)) => return Err(RunError { kind: RunErrorKind::Internal, message: m, line: 0, column: 0 }),