Files
sausage/host/tests/it/cred_quickjs.rs
T

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();
}