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