#![cfg(unix)] use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; const FIXTURE: &str = r#" #include #include #include #include int32_t add_i32(int32_t a, int32_t b) { return a + b; } int64_t add_i64(int64_t a, int64_t b) { return a + b; } bool negate(bool value) { return !value; } float half(float value) { return value / 2.0f; } double twice(double value) { return value * 2.0; } size_t add_usize(size_t a, size_t b) { return a + b; } uint64_t identity_u64(uint64_t value) { return value; } void delayed_fill(uint8_t *bytes, uint8_t value, size_t length) { usleep(50000); if (length > 0) bytes[0] = value; } uint32_t sum_bytes(const uint8_t *bytes, size_t length) { uint32_t total = 0; for (size_t i = 0; i < length; i++) total += bytes[i]; return total; } void fill_bytes(uint8_t *bytes, uint8_t value, size_t length) { for (size_t i = 0; i < length; i++) bytes[i] = value; } const char *greeting(void) { return "hello from ffi"; } void *identity(void *pointer) { return pointer; } typedef int (*int_fn)(int); static int inc(int value) { return value + 1; } int_fn get_inc(void) { return inc; } int apply_fn(int_fn function, int value) { return function(value); } "#; fn fixture() -> (tempfile::TempDir, PathBuf) { let dir = tempfile::tempdir().unwrap(); let source = dir.path().join("fixture.c"); let library = dir.path().join("libfixture.so"); fs::write(&source, FIXTURE).unwrap(); let output = Command::new("cc") .args(["-shared", "-fPIC", "-o"]) .arg(&library) .arg(&source) .output() .unwrap(); assert!( output.status.success(), "cc failed: {}", String::from_utf8_lossy(&output.stderr) ); (dir, library) } fn run(dir: &tempfile::TempDir, source: &str, args: &[String]) -> Output { let main = dir.path().join("main.js"); fs::write(&main, source).unwrap(); Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .args(args) .arg(&main) .output() .unwrap() } fn script(library: &Path, body: &str) -> String { format!( "import {{ dlopen }} from \"avril:ffi\";\nconst lib = dlopen({}, {{\n add: {{ parameters: [\"i32\", \"i32\"], result: \"i32\", name: \"add_i32\" }},\n add64: {{ parameters: [\"i64\", \"i64\"], result: \"i64\", name: \"add_i64\" }},\n negate: {{ parameters: [\"bool\"], result: \"bool\" }},\n half: {{ parameters: [\"f32\"], result: \"f32\" }},\n twice: {{ parameters: [\"f64\"], result: \"f64\" }},\n addSize: {{ parameters: [\"usize\", \"usize\"], result: \"usize\", name: \"add_usize\" }},\n idU64: {{ parameters: [\"u64\"], result: \"u64\", name: \"identity_u64\" }},\n slowSum: {{ parameters: [\"buffer\", \"usize\"], result: \"u32\", name: \"sum_bytes\", nonblocking: true }},\n slowFill: {{ parameters: [\"buffer\", \"u8\", \"usize\"], result: \"void\", name: \"delayed_fill\", nonblocking: true }},\n sum: {{ parameters: [\"buffer\", \"usize\"], result: \"u32\", name: \"sum_bytes\" }},\n fill: {{ parameters: [\"buffer\", \"u8\", \"usize\"], result: \"void\", name: \"fill_bytes\" }},\n greeting: {{ parameters: [], result: \"pointer\" }},\n identity: {{ parameters: [\"pointer\"], result: \"pointer\" }},\n getInc: {{ parameters: [], result: \"function\", name: \"get_inc\" }},\n apply: {{ parameters: [\"function\", \"i32\"], result: \"i32\", name: \"apply_fn\" }},\n slowAdd: {{ parameters: [\"i32\", \"i32\"], result: \"i32\", name: \"add_i32\", nonblocking: true }},\n}});\n{body}\n", serde_json::to_string(&library.to_string_lossy()).unwrap() ) } #[test] fn ffi_calls_native_symbols() { let (dir, library) = fixture(); let source = script( &library, r#" import { UnsafeFnPointer, UnsafePointer, UnsafePointerView } from "avril:ffi"; const out = []; out.push(lib.symbols.add(40, 2)); out.push(lib.symbols.add64(20n, 22n)); out.push(lib.symbols.negate(false)); out.push(lib.symbols.half(21)); out.push(lib.symbols.twice(21)); out.push(lib.symbols.addSize(20n, 22n)); out.push(lib.symbols.idU64(9223372036854775808n)); out.push(lib.symbols.idU64(18446744073709551615n)); const bytes = new Uint8Array([1, 2, 3, 36]); out.push(lib.symbols.sum(bytes, 4)); lib.symbols.fill(bytes, 9, 4); out.push(lib.symbols.sum(bytes, 4)); out.push(new UnsafePointerView(lib.symbols.greeting()).getCString()); const [pointer, view] = UnsafePointer.create(4); new UnsafePointerView(pointer).setInt32(42); out.push(view[0]); out.push(UnsafePointer.value(lib.symbols.identity(pointer)) === UnsafePointer.value(pointer)); const inc = new UnsafeFnPointer(lib.symbols.getInc(), { parameters: ["i32"], result: "i32" }); out.push(inc.call(41)); out.push(lib.symbols.apply(inc, 41)); out.push(await lib.symbols.slowAdd(40, 2)); out.push(await lib.symbols.slowSum(new Uint8Array(0), 0)); out.push(new UnsafePointerView(UnsafePointer.create(0)[0]).getArrayBuffer(0).byteLength); const nb = new Uint8Array([0, 0]); const pending = lib.symbols.slowFill(nb, 9, 2); nb[1] = 77; await pending; out.push(nb.join(",")); console.log(out.join(" ")); lib.close(); try { lib.symbols.add(1, 2); console.log("still open"); } catch { console.log("closed"); } "#, ); let output = run(&dir, &source, &[]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!( String::from_utf8_lossy(&output.stdout).trim(), "42 42 true 10.5 42 42 9223372036854775808 18446744073709551615 42 36 hello from ffi 42 true 42 42 42 0 0 9,77\nclosed" ); } #[test] fn ffi_requires_authority_and_honors_pledge() { let (dir, library) = fixture(); let source = script(&library, "console.log(lib.symbols.add(40, 2));"); let denied = run(&dir, &source, &["-0".into()]); assert!(!denied.status.success()); assert!( String::from_utf8_lossy(&denied.stderr).contains("PermissionDenied: ffi"), "{}", String::from_utf8_lossy(&denied.stderr) ); let deny_flag = run( &dir, &source, &[format!("--deny-ffi={}", library.display())], ); assert!(!deny_flag.status.success()); assert!( String::from_utf8_lossy(&deny_flag.stderr).contains("PermissionDenied: ffi"), "{}", String::from_utf8_lossy(&deny_flag.stderr) ); let pointers = "import { UnsafePointer, UnsafePointerView } from \"avril:ffi\";\nconst view = new UnsafePointerView(UnsafePointer.create(4)[0]);\nview.setInt32(42);\nconsole.log(view.getInt32());"; let pointers_denied_root = run(&dir, pointers, &["--deny-ffi=/".into()]); assert!(!pointers_denied_root.status.success()); assert!( String::from_utf8_lossy(&pointers_denied_root.stderr).contains("PermissionDenied: ffi"), "{}", String::from_utf8_lossy(&pointers_denied_root.stderr) ); let pointers_denied = run(&dir, pointers, &["-0".into()]); assert!(!pointers_denied.status.success()); assert!( String::from_utf8_lossy(&pointers_denied.stderr).contains("PermissionDenied: ffi"), "{}", String::from_utf8_lossy(&pointers_denied.stderr) ); let pointers_allowed = run( &dir, pointers, &["-0".into(), format!("--allow-ffi={}", library.display())], ); assert!( pointers_allowed.status.success(), "{}", String::from_utf8_lossy(&pointers_allowed.stderr) ); assert_eq!( String::from_utf8_lossy(&pointers_allowed.stdout).trim(), "42" ); let allowed = run( &dir, &source, &[format!("--allow-ffi={}", library.display())], ); assert!( allowed.status.success(), "{}", String::from_utf8_lossy(&allowed.stderr) ); assert_eq!(String::from_utf8_lossy(&allowed.stdout).trim(), "42"); let source = format!( "import {{ pledge }} from \"avril:std\";\nimport {{ dlopen }} from \"avril:ffi\";\npledge({{ ffi: false }});\ntry {{ dlopen({}, {{ add: {{ parameters: [\"i32\", \"i32\"], result: \"i32\", name: \"add_i32\" }} }}); console.log(\"allowed\"); }} catch (error) {{ console.log(error.message.includes(\"PermissionDenied: ffi\") ? \"denied\" : error.message); }}", serde_json::to_string(&library.to_string_lossy()).unwrap() ); let output = run( &dir, &source, &[format!("--allow-ffi={}", library.display())], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "denied"); } #[test] fn ffi_rejects_bad_calls() { let (dir, library) = fixture(); let source = script( &library, r#" const out = []; try { lib.symbols.add(2147483648, 0); } catch (error) { out.push(error instanceof TypeError ? "range" : error.message); } try { lib.symbols.add(1); } catch (error) { out.push("arity"); } try { lib.symbols.add("7", 0); } catch (error) { out.push(error instanceof TypeError ? "string" : error.message); } const victim = new ArrayBuffer(16); const view = new Uint8Array(victim); try { lib.symbols.fill(view, { valueOf() { victim.transfer(8); return 1; } }, 16); out.push("coerced"); } catch (error) { out.push(error instanceof TypeError ? "detach" : error.message); } import { DynamicLibrary } from "avril:ffi"; try { new DynamicLibrary(1, {}); out.push("forged"); } catch { out.push("forge"); } const buf2 = new ArrayBuffer(4096); const view2 = new Uint8Array(buf2); const realMap = Array.prototype.map; Array.prototype.map = function (fn) { const mapped = Reflect.apply(realMap, this, [fn]); if (mapped.length === 3 && mapped[0] === view2) Object.defineProperty(mapped, 2, { get() { buf2.transfer(16); return 4096; } }); return mapped; }; try { lib.symbols.fill(view2, 65, 4096); out.push(view2[0] === 65 ? "getter" : "getter-fail"); } finally { Array.prototype.map = realMap; } console.log(out.join(" ")); "#, ); let output = run(&dir, &source, &[]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!( String::from_utf8_lossy(&output.stdout).trim(), "range arity string detach forge getter" ); let missing = format!( "import {{ dlopen }} from \"avril:ffi\";\ndlopen({}, {{ nope: {{ parameters: [], result: \"void\" }} }});", serde_json::to_string(&library.to_string_lossy()).unwrap() ); let output = run(&dir, &missing, &[]); assert!(!output.status.success()); } #[test] fn definitions_keep_getter_order_and_pointer_objects_keep_their_shape() { let (dir, library) = fixture(); let source = format!( "const libraryPath = {};\n", serde_json::to_string(&library.to_string_lossy()).unwrap() ) + r#" import { dlopen, Pointer, UnsafePointer, UnsafePointerView, UnsafeFnPointer, DynamicLibrary } from "avril:ffi"; import * as ffi from "avril:ffi"; if (Object.keys(ffi).sort().join() !== "DynamicLibrary,Pointer,UnsafeFnPointer,UnsafePointer,UnsafePointerView,dlopen") { throw new Error("public namespace"); } for (const name of ["Pointer", "UnsafePointerView", "UnsafeFnPointer", "DynamicLibrary"]) { const constructor = ffi[name]; if (constructor.name !== name || constructor.prototype.constructor !== constructor || Object.hasOwn(constructor, Symbol.hasInstance) || !(Object.create(constructor.prototype) instanceof constructor)) { throw new Error(`constructor shape: ${name}`); } } const order = []; let iterations = 0; const parameters = ["i32", "i32"]; parameters[Symbol.iterator] = function* () { if (this !== parameters) throw new Error("parameter iterator receiver"); iterations++; yield "i32"; yield "i32"; }; const definitions = { get add() { order.push("symbol"); return { get parameters() { order.push("parameters"); return parameters; }, get result() { order.push("result"); return "i32"; }, get nonblocking() { order.push("nonblocking"); return false; }, get name() { order.push("name"); return "add_i32"; }, }; }, }; const library = dlopen({ [Symbol.toPrimitive](hint) { if (hint !== "string") throw new Error("path hint"); order.push("path"); return libraryPath; }, }, definitions); if (order.join() !== "symbol,parameters,result,nonblocking,name,path" || iterations !== 2 || library.symbols.add(20, 22) !== 42) { throw new Error("definition conversion"); } const pointer = new Pointer("0"); if (Reflect.ownKeys(pointer).length !== 0 || !Object.isFrozen(pointer) || UnsafePointer.value(pointer) !== 0n || Object.keys(library).join() !== "symbols" || !Object.isFrozen(library) || !Object.isFrozen(library.symbols)) { throw new Error("resource shape"); } if (dlopen.length !== 2 || Pointer.length !== 1 || UnsafePointerView.length !== 1 || UnsafeFnPointer.length !== 2 || DynamicLibrary.length !== 3 || library.symbols.add.length !== 0 || UnsafeFnPointer.prototype.call.length !== 0) { throw new Error("reflected arity"); } class DerivedPointer extends Pointer {} const derived = new DerivedPointer(0n); const proxiedConstructor = new Proxy(Pointer, {}); if (!(derived instanceof Pointer) || !(derived instanceof DerivedPointer) || !(derived instanceof proxiedConstructor) || UnsafePointer.value(derived) !== 0n || !(new proxiedConstructor(0n) instanceof Pointer) || !(library instanceof DynamicLibrary)) { throw new Error("constructor identity"); } const fn = new UnsafeFnPointer(pointer, { parameters: [], result: "void" }); if (!Object.isFrozen(fn) || fn.address !== 0n || Object.keys(fn).join() !== "address" || !(fn instanceof UnsafeFnPointer) || !(new UnsafePointerView(pointer) instanceof UnsafePointerView)) { throw new Error("pointer instance shape"); } let forgedRejected = false; try { UnsafePointer.value(Object.create(Pointer.prototype)); } catch (error) { forgedRejected = error instanceof TypeError; } if (!forgedRejected) throw new Error("forged pointer accepted"); if (library.close() !== undefined || library.close() !== undefined || !library.closed) { throw new Error("library close"); } console.log("preserved"); "#; let output = run(&dir, &source, &[]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"preserved\n"); }