238 lines
7.3 KiB
Rust
238 lines
7.3 KiB
Rust
//! Behavioral contract for the credential-only isolate.
|
|
use sausage::cred::{
|
|
quickjs::QuickJs,
|
|
script::{ErrorKind, Limits, Script},
|
|
};
|
|
use std::time::{Duration, Instant};
|
|
static TEST_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
fn run(s: &str) -> Result<String, sausage::cred::script::Error> {
|
|
QuickJs::default().evaluate(s, "null", None, Limits::default())
|
|
}
|
|
#[test]
|
|
fn modules_promises_and_json() {
|
|
let _guard = TEST_GUARD.lock().unwrap();
|
|
assert_eq!(
|
|
QuickJs::default()
|
|
.evaluate(
|
|
"JSON.parse=()=>false; export default x=>x",
|
|
"42",
|
|
None,
|
|
Limits::default()
|
|
)
|
|
.unwrap(),
|
|
"42"
|
|
);
|
|
let error = run("export default ()=>{throw Error('DO NOT ECHO')}").unwrap_err();
|
|
assert!(!error.message.contains("DO NOT ECHO"));
|
|
assert_eq!(
|
|
QuickJs::default()
|
|
.evaluate(
|
|
"export default x=>({value:x}); export const other=x=>x+1",
|
|
"42",
|
|
None,
|
|
Limits::default()
|
|
)
|
|
.unwrap(),
|
|
"{\"value\":42}"
|
|
);
|
|
assert_eq!(
|
|
QuickJs::default()
|
|
.evaluate(
|
|
"export const other=x=>x+1",
|
|
"42",
|
|
Some("other"),
|
|
Limits::default()
|
|
)
|
|
.unwrap(),
|
|
"43"
|
|
);
|
|
assert_eq!(
|
|
run("await Promise.resolve(); export default async x=>{await Promise.resolve();return 42}")
|
|
.unwrap(),
|
|
"42"
|
|
);
|
|
for (s, k) in [
|
|
("export default (", ErrorKind::Syntax),
|
|
("export default ()=>{throw Error('oops')}", ErrorKind::Throw),
|
|
("export default ()=>Promise.reject('no')", ErrorKind::Throw),
|
|
("export default ()=>undefined", ErrorKind::NonJson),
|
|
("export default ()=>Symbol()", ErrorKind::NonJson),
|
|
("export default ()=>()=>1", ErrorKind::NonJson),
|
|
(
|
|
"export default ()=>{let a={};a.a=a;return a}",
|
|
ErrorKind::NonJson,
|
|
),
|
|
("export const x=1", ErrorKind::Engine),
|
|
("export default 1", ErrorKind::Engine),
|
|
("export default ()=>new Promise(()=>{})", ErrorKind::Engine),
|
|
] {
|
|
assert_eq!(run(s).unwrap_err().kind, k, "{s}");
|
|
}
|
|
assert_eq!(
|
|
QuickJs::default()
|
|
.evaluate("export default x=>x", "invalid", None, Limits::default())
|
|
.unwrap_err()
|
|
.kind,
|
|
ErrorKind::Syntax
|
|
);
|
|
}
|
|
#[test]
|
|
fn sandbox_and_isolation() {
|
|
let _guard = TEST_GUARD.lock().unwrap();
|
|
for name in [
|
|
"Date",
|
|
"Math.random",
|
|
"fetch",
|
|
"setTimeout",
|
|
"setInterval",
|
|
"WebSocket",
|
|
"XMLHttpRequest",
|
|
"std",
|
|
"os",
|
|
"fs",
|
|
"Deno",
|
|
"__bootstrap",
|
|
"console",
|
|
"performance",
|
|
"require",
|
|
"process",
|
|
"TextEncoder",
|
|
"Buffer",
|
|
"Blob",
|
|
"File",
|
|
"crypto",
|
|
] {
|
|
assert_eq!(
|
|
run(&format!("export default ()=>typeof {name}")).unwrap(),
|
|
"\"undefined\"",
|
|
"{name}"
|
|
);
|
|
}
|
|
assert!(run("import x from 'fs'; export default ()=>x").is_err());
|
|
assert!(run("import {core} from 'ext:core/mod.js'; export default ()=>typeof core").is_err());
|
|
assert_eq!(
|
|
run("export default ()=>import('fs')").unwrap_err().kind,
|
|
ErrorKind::Throw
|
|
);
|
|
run("export default ()=>{globalThis.leak=1;btoa.leak=1;return true}").unwrap();
|
|
assert_eq!(
|
|
run("export default ()=>[typeof leak,typeof btoa.leak]").unwrap(),
|
|
"[\"undefined\",\"undefined\"]"
|
|
);
|
|
for (expr, expected) in [
|
|
("btoa('user:pass')", "\"dXNlcjpwYXNz\""),
|
|
("atob('dXNlcjpwYXNz')", "\"user:pass\""),
|
|
("btoa('')", "\"\""),
|
|
("atob('AP+A') === '\\x00\\xff\\x80'", "true"),
|
|
] {
|
|
assert_eq!(
|
|
run(&format!("export default ()=>{expr}")).unwrap(),
|
|
expected
|
|
);
|
|
}
|
|
for expr in ["atob('!')", "btoa('雪')"] {
|
|
assert_eq!(
|
|
run(&format!("export default ()=>{expr}")).unwrap_err().kind,
|
|
ErrorKind::Throw
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn independent_deadlines() {
|
|
let _guard = TEST_GUARD.lock().unwrap();
|
|
// Exclude the process-wide platform initialization from the overlap check.
|
|
assert_eq!(run("export default ()=>true").unwrap(), "true");
|
|
let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
|
|
let other = barrier.clone();
|
|
let runaway = std::thread::spawn(move || {
|
|
other.wait();
|
|
assert_eq!(
|
|
QuickJs::default()
|
|
.evaluate(
|
|
"export default ()=>{while(true){}}",
|
|
"null",
|
|
None,
|
|
Limits {
|
|
max_time_ms: Some(1000),
|
|
..Limits::default()
|
|
}
|
|
)
|
|
.unwrap_err()
|
|
.kind,
|
|
ErrorKind::Limit
|
|
);
|
|
});
|
|
barrier.wait();
|
|
// The quick call keeps its default 100 ms execution deadline.
|
|
assert_eq!(run("export default ()=>true").unwrap(), "true");
|
|
assert!(
|
|
!runaway.is_finished(),
|
|
"the quick call must finish before the runaway call"
|
|
);
|
|
runaway.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn clamped_limits() {
|
|
let _guard = TEST_GUARD.lock().unwrap();
|
|
for limits in [
|
|
Limits {
|
|
max_time_ms: Some(0),
|
|
max_memory_bytes: Some(0),
|
|
},
|
|
Limits {
|
|
max_time_ms: Some(u64::MAX),
|
|
max_memory_bytes: Some(usize::MAX),
|
|
},
|
|
] {
|
|
match QuickJs::default().evaluate("export default ()=>true", "null", None, limits) {
|
|
Ok(value) => assert_eq!(value, "true"),
|
|
Err(error) => assert_eq!(error.kind, ErrorKind::Limit),
|
|
}
|
|
}
|
|
assert_eq!(
|
|
QuickJs::default().evaluate(
|
|
"export default ()=>{let a=[];while(1)a.push('abcdefghijklmnopqrstuvwxyz'+a.length)}",
|
|
"null",
|
|
None,
|
|
Limits {
|
|
max_time_ms: Some(1000),
|
|
max_memory_bytes: Some(1024 * 1024)
|
|
}
|
|
)
|
|
.unwrap_err()
|
|
.kind,
|
|
ErrorKind::Limit
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn limits_and_parallel_calls() {
|
|
let _guard = TEST_GUARD.lock().unwrap();
|
|
let _runtime = QuickJs::default();
|
|
let mut workers = Vec::new();
|
|
for s in [
|
|
"export default ()=>{while(true){}}",
|
|
"while(true){};export default x=>x",
|
|
"export default ()=>({toJSON(){while(true){}}})",
|
|
"export default ()=>{throw {get message(){while(true){}}}}",
|
|
"export default ()=>{function f(){return f()+1}return f()}",
|
|
"export default ()=>new Promise(()=>{function f(){Promise.resolve().then(f)}f()})",
|
|
"export default ()=>{let a=[];while(1)a.push('abcdefghijklmnopqrstuvwxyz'+a.length)}",
|
|
] {
|
|
workers.push(std::thread::spawn(move || {
|
|
let start = Instant::now();
|
|
assert_eq!(run(s).unwrap_err().kind, ErrorKind::Limit);
|
|
assert!(start.elapsed() < Duration::from_secs(2));
|
|
}));
|
|
}
|
|
let normal = std::thread::spawn(|| match run("export default ()=>true") {
|
|
Ok(value) => assert_eq!(value, "true"),
|
|
Err(error) => assert_eq!(error.kind, ErrorKind::Limit),
|
|
});
|
|
for worker in workers {
|
|
worker.join().unwrap();
|
|
}
|
|
normal.join().unwrap();
|
|
}
|