mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 13:33:19 +00:00
JavaScript: source that eval and Function compile takes fuel before it is parsed
Direct and indirect eval, Function and host evaluators parsed and compiled their source before taking any fuel or checking the heap, so a script with a small fuel limit could make the interpreter parse megabytes of source in a handful of instructions. Interp::prepare_source now takes one unit of fuel for each UTF-16 code unit and checks that the source text fits in the heap before the source is flattened, parsed or compiled. A host evaluator's string takes the fuel for a third of its bytes before it is encoded, and the rest after. Limits::fuel and the maxFuel row in docs/mcp.md say so.
This commit is contained in:
@@ -466,6 +466,7 @@ fn function_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsRe
|
||||
.chain([parts.len().saturating_sub(1), HEAD.len() + MID.len() + TAIL.len(), body.len()])
|
||||
.fold(0usize, usize::saturating_add);
|
||||
interp.check_source_units(total).map_err(|e| interp.compile_error_to_abrupt(e))?;
|
||||
interp.prepare_source(total)?;
|
||||
let mut units = Vec::with_capacity(total);
|
||||
units.extend(HEAD.encode_utf16());
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
@@ -554,6 +555,7 @@ fn eval(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Valu
|
||||
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))?;
|
||||
interp.prepare_source(s.len())?;
|
||||
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)
|
||||
|
||||
+15
-3
@@ -397,11 +397,23 @@ impl Interp {
|
||||
let writes = strict_writes().unwrap_or_else(|| self.writes_mode());
|
||||
self.evaluated = self.evaluated.saturating_add(1);
|
||||
let tag = std::mem::replace(&mut self.source_tag, self.evaluated);
|
||||
let code = self.encode_source(&source).and_then(|units| self.compile_source_writes(&units, false, writes, crate::bytecode::CodeKind::Eval));
|
||||
// UTF-8 takes at most 3 bytes for each UTF-16 code unit, so the fuel for a third of the
|
||||
// bytes is taken before the source is encoded, and the rest after.
|
||||
let floor = source.len().div_ceil(3);
|
||||
let code = self.prepare_source(floor).map(|()| {
|
||||
let units = self.encode_source(&source);
|
||||
let rest = match &units {
|
||||
Ok(units) => self.prepare_source(units.len().saturating_sub(floor)),
|
||||
Err(_) => Ok(()),
|
||||
};
|
||||
rest.map(|()| units.and_then(|units| self.compile_source_writes(&units, false, writes, crate::bytecode::CodeKind::Eval)))
|
||||
});
|
||||
let code = code.and_then(|c| c);
|
||||
self.source_tag = tag;
|
||||
let r = match code {
|
||||
Ok(code) => self.run_global(code, true),
|
||||
Err(e) => Err(self.compile_error_to_abrupt(e)),
|
||||
Ok(Ok(code)) => self.run_global(code, true),
|
||||
Ok(Err(e)) => Err(self.compile_error_to_abrupt(e)),
|
||||
Err(limit) => Err(limit),
|
||||
};
|
||||
end();
|
||||
r
|
||||
|
||||
@@ -540,6 +540,27 @@ fn error_sources_count_from_the_last_script_and_code_from_an_earlier_one_is_unkn
|
||||
assert_eq!(r.error_source(), 0, "a syntax error is in the script");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_evaluators_source_takes_fuel_before_it_is_parsed() {
|
||||
// The evaluator gives 131,072 spaces and a number: far more code units than the fuel left.
|
||||
let ends = Rc::new(Cell::new(0));
|
||||
let ended = ends.clone();
|
||||
let source: HostFunction = Rc::new(|_| Ok(Data::String(format!("{}7", " ".repeat(1 << 17)))));
|
||||
let item = HostItem::Evaluator { source, end: Rc::new(move || ended.set(ended.get() + 1)), strict_writes: Rc::new(|| None) };
|
||||
let mut r = realm(10_000);
|
||||
r.define_host_object("host", vec![("evaluate".into(), item)]).unwrap();
|
||||
let e = r.run("host.evaluate()").unwrap_err();
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel));
|
||||
assert_eq!(ends.get(), 1, "the evaluator's end runs once");
|
||||
// This fuel covers a third of the source's bytes but not all of its code units.
|
||||
r.set_limits(Limits { fuel: 60_000, ..Limits::default() });
|
||||
assert_eq!(r.run("host.evaluate()").unwrap_err().kind, RunErrorKind::Limit(LimitKind::Fuel));
|
||||
assert_eq!(ends.get(), 2);
|
||||
r.set_limits(Limits { fuel: 1_000_000, ..Limits::default() });
|
||||
assert_eq!(r.run("host.evaluate()"), Ok(JsValue::Number(7.0)));
|
||||
assert_eq!(ends.get(), 3);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
/// A slice size does not change what a script computes.
|
||||
#[test]
|
||||
|
||||
+19
-1
@@ -9,7 +9,8 @@
|
||||
//! Before each instruction the interpreter takes one unit of fuel, checks the interrupt flag and,
|
||||
//! when the heap has grown past its threshold, collects garbage. Running out of fuel, the heap
|
||||
//! limit, the frame limit, the re-entry limit and the interrupt flag each end the run with an
|
||||
//! [`Abrupt::Limit`] that unwinds every frame without running catch or finally blocks.
|
||||
//! [`Abrupt::Limit`] that unwinds every frame without running catch or finally blocks. Source
|
||||
//! compiled during a run takes a unit of fuel for each code unit before it is parsed.
|
||||
//!
|
||||
//! Collection runs only between instructions. An operation that may run JavaScript in the middle
|
||||
//! keeps its operands on the operand stack until it is done, so everything it holds is reachable
|
||||
@@ -617,6 +618,22 @@ impl Interp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prepares for parsing `units` code units of source at run time: takes a unit of fuel for
|
||||
/// each code unit and checks that the source text fits in the heap, before any of it is
|
||||
/// flattened, parsed or compiled. Running out of fuel uses up what was left.
|
||||
pub(crate) fn prepare_source(&mut self, units: usize) -> JsResult<()> {
|
||||
let fuel = u64::try_from(units).unwrap_or(u64::MAX);
|
||||
if fuel > self.fuel {
|
||||
self.fuel = 0;
|
||||
return Err(Abrupt::Limit(LimitKind::Fuel));
|
||||
}
|
||||
self.fuel -= fuel;
|
||||
if self.interrupt.load(Ordering::Relaxed) {
|
||||
return Err(Abrupt::Limit(LimitKind::Interrupted));
|
||||
}
|
||||
self.reserve_heap(string_bytes(units))
|
||||
}
|
||||
|
||||
/// Removes the [`Self::finally_marks`] entries of `finally` blocks that `break`, `continue` or
|
||||
/// `return` left: those whose stack slot no longer holds their exception.
|
||||
fn drop_left_finally_marks(&mut self) {
|
||||
@@ -2397,6 +2414,7 @@ impl Interp {
|
||||
};
|
||||
let strict = self.strict();
|
||||
self.check_source_units(s.len()).map_err(|e| self.compile_error_to_abrupt(e))?;
|
||||
self.prepare_source(s.len())?;
|
||||
let units = s.flat();
|
||||
let code = self.compile_source(&units, strict, CodeKind::Eval).map_err(|e| self.compile_error_to_abrupt(e))?;
|
||||
self.charge_code(&code, declaration_bytes(&code).saturating_add(ENV_BYTES))?;
|
||||
|
||||
@@ -500,6 +500,19 @@ fn declarations_take_fuel() {
|
||||
assert_eq!(limit(&mut realm, &format!("Function('var {vars};')();")), LimitKind::Fuel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_compiled_during_a_run_takes_fuel_before_it_is_parsed() {
|
||||
// Doubling a string 17 times makes 131,072 spaces in about a hundred instructions, so only
|
||||
// the compiled source can use up the fuel.
|
||||
let build = "var s = ' '; for (var i = 0; i < 17; i++) s += s;";
|
||||
let mut realm = small(Limits { fuel: 10_000, ..Limits::default() });
|
||||
for call in ["eval(s)", "(0, eval)(s)", "Function(s)", "new Function('a', s)"] {
|
||||
assert_eq!(limit(&mut realm, &format!("{build} {call};")), LimitKind::Fuel, "{call}");
|
||||
}
|
||||
let mut realm = small(Limits { fuel: 1_000_000, ..Limits::default() });
|
||||
assert_eq!(realm.run(&format!("{build} eval(s + '1') + (0, eval)(s + '2') + Function(s + 'return 3')()")), Ok(JsValue::Number(6.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lower_heap_limit_applies_to_what_is_already_live() {
|
||||
// A heap already past the new limit ends the next run.
|
||||
@@ -823,13 +836,14 @@ fn long_property_names_count_toward_the_heap_after_their_strings_are_collected()
|
||||
// Each name is a 16,385-unit buffer that only the object's key or the function's binding
|
||||
// holds once the string that made it is garbage, so about 32 of them fill the heap well before
|
||||
// the fuel runs out. In the second script the property lookup flattens a long string each
|
||||
// time, so collections run.
|
||||
// time, so collections run. Each eval there also takes fuel for its source, so that run has
|
||||
// more fuel.
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 20_000, ..Limits::default() });
|
||||
let src = "var s = 'x'; while (s.length < 10000) s += s; var o = {}; for (var i = 0; ; i++) o[s + i] = 1;";
|
||||
assert_eq!(limit(&mut realm, src), LimitKind::Heap);
|
||||
let src = "var s = 'x'; while (s.length < 10000) s += s; \
|
||||
(function () { for (var i = 0; ; i++) { eval('var ' + s + i + ' = 1'); ({})[s + i]; } })();";
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 20_000, ..Limits::default() });
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 5_000_000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, src), LimitKind::Heap);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ use crate::{Options, SyntaxError};
|
||||
/// cannot catch it, and their `finally` blocks do not run.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Limits {
|
||||
/// Instructions a run may execute.
|
||||
/// Instructions a run may execute. Source that `eval`, `Function` or a host evaluator
|
||||
/// compiles during the run also takes one unit for each UTF-16 code unit, before it is parsed.
|
||||
pub fuel: u64,
|
||||
/// Estimated bytes the heap (objects, environments and the strings they hold) may reach
|
||||
/// after a collection. A string longer than a quarter of this many code units cannot be
|
||||
|
||||
+1
-1
@@ -1826,7 +1826,7 @@ VectorCraft sets these limits itself:
|
||||
| Included and evaluated files | 64 in all, 16 levels deep, 1,024 paths looked up |
|
||||
| Heap | 256 MiB (128 MiB on the web) |
|
||||
| Host calls | 2,000,000 |
|
||||
| `maxFuel`, instructions in all | 1 to 100,000,000,000; 1,000,000,000 by default |
|
||||
| `maxFuel`, instructions in all, plus one for each UTF-16 code unit of source that `eval`, `Function` or an evaluated file compiles during the run | 1 to 100,000,000,000; 1,000,000,000 by default |
|
||||
| Instructions without a pause | 200,000,000 |
|
||||
| Nesting depth | 64 levels |
|
||||
| A value crossing to or from a command | 16 MiB and 1,048,576 entries |
|
||||
|
||||
Reference in New Issue
Block a user