From ba9e78ebfdb01fa8fcd8e5a3a7beb8f11ef74d69 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 12:45:02 -0800 Subject: [PATCH] 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. --- crates/js/src/builtins.rs | 3 ++- crates/js/src/interp.rs | 19 +++++++++++++++++++ crates/js/src/interp_tests.rs | 33 +++++++++++++++++++++++++++++++++ crates/js/src/realm.rs | 4 ++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 02f2364e9..3274e2873 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -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 = 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 { 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) diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 5f1f2890a..4d4b3d039 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -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, 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, 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()?; diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index bbb3a2cb7..1b2a192a7 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -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(); diff --git a/crates/js/src/realm.rs b/crates/js/src/realm.rs index 955383b18..2cfa19231 100644 --- a/crates/js/src/realm.rs +++ b/crates/js/src/realm.rs @@ -173,8 +173,8 @@ impl Realm { interp.fuel = interp.limits.fuel; interp.source_units = 0; interp.throw_site = None; - let units: Vec = 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 }),