The platform part of work 76627b6f1..6941c0e86, as one step that builds (makepad-platform checks for macOS and wasm32): - script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix - script: port Octoscript WS1 — worklist equality with fuel/deadline/work bail, uncaught-error bail, allow_debug_output - script: port Octoscript parser/tokenizer/control-flow VM patches (ws2) - script: port Octoscript heap/string/array/object hardening onto upstream's allocation budget - script: port orphaned Octoscript VM hardening (execution caps, clear_type_methods, parser diagnostics) - script: keep silenced streaming evals running past uncaught errors - script: the Octoscript hardening keeps a Makepad host's semantics and its speed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
150 lines
5.3 KiB
Rust
150 lines
5.3 KiB
Rust
//! What a Makepad host (widgets, Studio, the DSL of every app) relies on and a
|
|
//! sandboxing host opts out of: an uncaught error is reported and the
|
|
//! evaluation continues, an error raised while no script ran belongs to no
|
|
//! evaluation, and an error inside a callback a native invokes never unwinds
|
|
//! into the frames of the run that called the native.
|
|
|
|
use makepad_script::*;
|
|
|
|
fn test_vm() -> ScriptVm<'static> {
|
|
let host = Box::leak(Box::new(ScriptVmHost::new((), ())));
|
|
let mut vm = ScriptVm {
|
|
host,
|
|
bx: Box::new(ScriptVmBase::new()),
|
|
};
|
|
vm.bx.captured_errors = Some(Vec::new());
|
|
vm
|
|
}
|
|
|
|
fn eval(vm: &mut ScriptVm, name: &str, code: &str) -> ScriptValue {
|
|
vm.eval(ScriptMod {
|
|
file: format!("trusted_host_{name}.splash"),
|
|
code: format!("{code}\n;"),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
/// A host-side error with no script running: Rust calls a value that is not a
|
|
/// function (a missing callback).
|
|
fn raise_a_host_side_error(vm: &mut ScriptVm) {
|
|
let not_a_fn: ScriptValue = 7.into();
|
|
assert!(vm.call(not_a_fn, &[]).is_err());
|
|
}
|
|
|
|
/// `retain` calls its closure back on the same thread, so the closure runs in
|
|
/// a nested root frame while the outer run is still live under the native.
|
|
/// The closure fails on element 2; `n` counts the elements it finished.
|
|
const CALLBACK_FAILS_ON_2: &str = "fn(v){ if v == 2 { let f = 1\n f() }\n n = n + 1\n true }";
|
|
|
|
#[test]
|
|
fn the_module_keeps_evaluating_past_an_uncaught_error() {
|
|
let vm = &mut test_vm();
|
|
let value = eval(vm, "continue", "let f = 1\nf()\n42");
|
|
assert_eq!(value.as_number(), Some(42.0), "{value:?}");
|
|
assert_eq!(vm.take_errors().len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_returned_error_value_can_be_inspected_without_try() {
|
|
let vm = &mut test_vm();
|
|
let value = eval(vm, "inspect", "let f = 1\nlet r = f()\nif r == nil { 1 } else { 2 }");
|
|
assert_eq!(value.as_number(), Some(2.0), "{value:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_host_side_error_does_not_end_the_next_evaluation() {
|
|
for bail_on_uncaught_error in [false, true] {
|
|
let vm = &mut test_vm();
|
|
vm.bx.bail_on_uncaught_error = bail_on_uncaught_error;
|
|
raise_a_host_side_error(vm);
|
|
let value = eval(vm, "stale", "40 + 2");
|
|
assert_eq!(value.as_number(), Some(42.0), "bail={bail_on_uncaught_error} {value:?}");
|
|
// reported once, at the boundary, not lost
|
|
assert_eq!(vm.take_errors().len(), 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_host_side_error_is_not_caught_by_the_next_evaluations_try() {
|
|
let vm = &mut test_vm();
|
|
raise_a_host_side_error(vm);
|
|
let value = eval(vm, "stale_try", "try { 1 } catch { 2 }");
|
|
assert_eq!(value.as_number(), Some(1.0), "{value:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_host_side_error_does_not_end_the_next_callback() {
|
|
let vm = &mut test_vm();
|
|
vm.bx.bail_on_uncaught_error = true;
|
|
let callback = eval(vm, "callback", "fn(x){ return x + 1 }");
|
|
raise_a_host_side_error(vm);
|
|
let value = vm.call(callback, &[41.into()]);
|
|
assert_eq!(value.as_number(), Some(42.0), "{value:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn an_error_in_a_native_callback_does_not_unwind_into_the_outer_run() {
|
|
let vm = &mut test_vm();
|
|
// the outer frame owns a try while the native calls back
|
|
let value = eval(
|
|
vm,
|
|
"outer_try",
|
|
&format!(
|
|
"let a = [1, 2, 3]\nlet n = 0\nlet r = try {{ a.retain({CALLBACK_FAILS_ON_2})\n 7 }} catch {{ 42 }}\nr * 1000 + n"
|
|
),
|
|
);
|
|
assert_eq!(value.as_number(), Some(7003.0), "{value:?} {:#?}", vm.take_errors());
|
|
|
|
// a script fn owns the try, so a non-root frame sits under the native too
|
|
let vm = &mut test_vm();
|
|
let value = eval(
|
|
vm,
|
|
"fn_owns_try",
|
|
&format!(
|
|
"let a = [1, 2, 3]\nlet n = 0\nlet g = fn(){{ return try {{ a.retain({CALLBACK_FAILS_ON_2})\n 7 }} catch {{ 42 }} }}\nlet r = g()\nr * 1000 + n"
|
|
),
|
|
);
|
|
assert_eq!(value.as_number(), Some(7003.0), "{value:?} {:#?}", vm.take_errors());
|
|
}
|
|
|
|
#[test]
|
|
fn an_opted_in_bail_inside_a_native_callback_ends_the_callback_only() {
|
|
let vm = &mut test_vm();
|
|
vm.bx.bail_on_uncaught_error = true;
|
|
let value = eval(
|
|
vm,
|
|
"bail_in_callback",
|
|
&format!(
|
|
"let a = [1, 2, 3]\nlet n = 0\nlet r = try {{ a.retain({CALLBACK_FAILS_ON_2})\n 7 }} catch {{ 42 }}\nr * 1000 + n"
|
|
),
|
|
);
|
|
// `retain` stops at the failed callback; the outer run is intact and its
|
|
// frames were not unwound from inside the nested one.
|
|
assert_eq!(value.as_number(), Some(7001.0), "{value:?} {:#?}", vm.take_errors());
|
|
let errors = vm.take_errors();
|
|
assert!(
|
|
!errors.iter().any(|e| e.contains("root call reached")),
|
|
"{errors:#?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn checked_index_accepts_exactly_the_integral_in_range_numbers() {
|
|
let index = |v: f64| ScriptValue::from_f64(v).checked_index();
|
|
assert_eq!(index(0.0), Some(0));
|
|
assert_eq!(index(-0.0), Some(0));
|
|
assert_eq!(index(7.0), Some(7));
|
|
assert_eq!(index(9_007_199_254_740_992.0), Some(9_007_199_254_740_992));
|
|
for rejected in [
|
|
1.5,
|
|
-1.0,
|
|
f64::NAN,
|
|
f64::INFINITY,
|
|
f64::NEG_INFINITY,
|
|
18_446_744_073_709_551_616.0, // 2^64: saturates to usize::MAX
|
|
1.0e300,
|
|
] {
|
|
assert_eq!(index(rejected), None, "{rejected}");
|
|
}
|
|
assert_eq!(ScriptValue::from_bool(true).checked_index(), None);
|
|
}
|