diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe25f664568b33..c9d98b5249b858 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,11 +164,13 @@ jobs: sudo apt-get update sudo apt-get install debootstrap - # Note: git, nc, strace, and time, are needed to run the benchmarks. - sudo debootstrap \ - --include=ca-certificates,curl,git,netcat-openbsd,strace,time \ - --no-merged-usr --variant=minbase bionic /sysroot \ - http://azure.archive.ubuntu.com/ubuntu + # `file` and `make` are needed to build libffi-sys. + # `curl` is needed to build rusty_v8. + # `git`, `nc`, `strace`, and `time`, are needed to run the benchmarks. + sudo debootstrap \ + --include=ca-certificates,curl,file,git,make,netcat-openbsd,strace,time \ + --no-merged-usr --variant=minbase bionic /sysroot \ + http://azure.archive.ubuntu.com/ubuntu sudo mount --rbind /dev /sysroot/dev sudo mount --rbind /sys /sysroot/sys sudo mount --rbind /home /sysroot/home diff --git a/Cargo.lock b/Cargo.lock index 4820b85414d377..09bcfceb4491b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,12 @@ dependencies = [ "regex", ] +[[package]] +name = "abort_on_panic" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955f37ac58af2416bac687c8ab66a4ccba282229bd7422a28d2281a5e66a6116" + [[package]] name = "adler" version = "1.0.2" @@ -568,6 +574,7 @@ dependencies = [ "deno_crypto", "deno_doc", "deno_fetch", + "deno_ffi", "deno_http", "deno_lint", "deno_net", @@ -629,6 +636,27 @@ dependencies = [ "winres", ] +[[package]] +name = "deno-libffi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a844ceea9e6233005c62dfe0bb7c1adab786ea78bab4ac1e5ea5cd2a5d47761" +dependencies = [ + "abort_on_panic", + "deno-libffi-sys", + "libc", +] + +[[package]] +name = "deno-libffi-sys" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a12b5205cc4f3944cefa192e851a0ee15c226b41733468b2660d4ab2bdf555" +dependencies = [ + "cc", + "make-cmd", +] + [[package]] name = "deno_bench_util" version = "0.7.0" @@ -725,6 +753,16 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "deno_ffi" +version = "0.1.0" +dependencies = [ + "deno-libffi", + "deno_core", + "dlopen", + "serde", +] + [[package]] name = "deno_http" version = "0.4.0" @@ -786,6 +824,7 @@ dependencies = [ "deno_core", "deno_crypto", "deno_fetch", + "deno_ffi", "deno_http", "deno_net", "deno_timers", @@ -2106,6 +2145,12 @@ dependencies = [ "syn 1.0.65", ] +[[package]] +name = "make-cmd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3831,12 +3876,9 @@ dependencies = [ ] [[package]] -name = "test_plugin" -version = "0.0.1" +name = "test_ffi" +version = "0.1.0" dependencies = [ - "deno_core", - "futures", - "serde", "test_util", ] diff --git a/Cargo.toml b/Cargo.toml index e90f005f29a2cd..8d0768697d489a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,13 @@ members = [ "cli", "core", "runtime", - "test_plugin", + "test_ffi", "test_util", "extensions/broadcast_channel", "extensions/console", "extensions/crypto", "extensions/fetch", + "extensions/ffi", "extensions/http", "extensions/net", "extensions/timers", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5d71f60375bbc0..22b734f13c4675 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,6 +25,7 @@ deno_console = { version = "0.13.0", path = "../extensions/console" } deno_core = { version = "0.95.0", path = "../core" } deno_crypto = { version = "0.27.0", path = "../extensions/crypto" } deno_fetch = { version = "0.36.0", path = "../extensions/fetch" } +deno_ffi = { version = "0.1.0", path = "../extensions/ffi" } deno_http = { version = "0.4.0", path = "../extensions/http" } deno_net = { version = "0.4.0", path = "../extensions/net" } deno_timers = { version = "0.11.0", path = "../extensions/timers" } diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs index 18506ccc856c6c..01c096a118d70e 100644 --- a/cli/diagnostics.rs +++ b/cli/diagnostics.rs @@ -54,7 +54,7 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[ "listen", "listenDatagram", "loadavg", - "openPlugin", + "dlopen", "osRelease", "ppid", "resolveDns", diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index a312f4bdad9e31..40ef4a76cdf00b 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -2131,7 +2131,7 @@ declare namespace Deno { | "write" | "net" | "env" - | "plugin" + | "ffi" | "hrtime"; /** The current status of the permission. */ @@ -2167,8 +2167,8 @@ declare namespace Deno { variable?: string; } - export interface PluginPermissionDescriptor { - name: "plugin"; + export interface FFIPermissionDescriptor { + name: "ffi"; } export interface HrtimePermissionDescriptor { @@ -2183,7 +2183,7 @@ declare namespace Deno { | WritePermissionDescriptor | NetPermissionDescriptor | EnvPermissionDescriptor - | PluginPermissionDescriptor + | FFIPermissionDescriptor | HrtimePermissionDescriptor; export interface PermissionStatusEventMap { diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 64efa0a2f1612f..ab4a6372926cd1 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -107,36 +107,44 @@ declare namespace Deno { swapFree: number; } - /** **UNSTABLE**: new API, yet to be vetted. - * - * Open and initialize a plugin. - * - * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; - * const rid = Deno.openPlugin("./path/to/some/plugin.so"); - * - * // The Deno.core namespace is needed to interact with plugins, but this is - * // internal so we use ts-ignore to skip type checking these calls. - * // @ts-ignore - * const { op_test_sync, op_test_async } = Deno.core.ops(); - * - * assert(op_test_sync); - * assert(op_test_async); - * - * // @ts-ignore - * const result = Deno.core.opSync("op_test_sync"); - * - * // @ts-ignore - * const result = await Deno.core.opAsync("op_test_sync"); - * ``` - * - * Requires `allow-plugin` permission. + /** All possible types for interfacing with foreign functions */ + export type NativeType = + | "void" + | "u8" + | "i8" + | "u16" + | "i16" + | "u32" + | "i32" + | "u64" + | "i64" + | "usize" + | "isize" + | "f32" + | "f64"; + + /** A foreign function as defined by its parameter and result types */ + export interface ForeignFunction { + parameters: NativeType[]; + result: NativeType; + } + + /** A dynamic library resource */ + export interface DynamicLibrary> { + /** All of the registered symbols along with functions for calling them */ + symbols: { [K in keyof S]: (...args: unknown[]) => unknown }; + + close(): void; + } + + /** **UNSTABLE**: new API * - * The plugin system is not stable and will change in the future, hence the - * lack of docs. For now take a look at the example - * https://github.com/denoland/deno/tree/main/test_plugin + * Opens a dynamic library and registers symbols */ - export function openPlugin(filename: string): number; + export function dlopen>( + filename: string, + symbols: S, + ): DynamicLibrary; /** The log category for a diagnostic message. */ export enum DiagnosticCategory { @@ -1043,14 +1051,14 @@ declare namespace Deno { */ net?: "inherit" | boolean | string[]; - /** Specifies if the `plugin` permission should be requested or revoked. - * If set to `"inherit"`, the current `plugin` permission will be inherited. - * If set to `true`, the global `plugin` permission will be requested. - * If set to `false`, the global `plugin` permission will be revoked. + /** Specifies if the `ffi` permission should be requested or revoked. + * If set to `"inherit"`, the current `ffi` permission will be inherited. + * If set to `true`, the global `ffi` permission will be requested. + * If set to `false`, the global `ffi` permission will be revoked. * * Defaults to "inherit". */ - plugin?: "inherit" | boolean; + ffi?: "inherit" | boolean; /** Specifies if the `read` permission should be requested or revoked. * If set to `"inherit"`, the current `read` permission will be inherited. @@ -1137,7 +1145,7 @@ declare interface WorkerOptions { * For example: `["https://deno.land", "localhost:8080"]`. */ net?: "inherit" | boolean | string[]; - plugin?: "inherit" | boolean; + ffi?: "inherit" | boolean; read?: "inherit" | boolean | Array; run?: "inherit" | boolean | Array; write?: "inherit" | boolean | Array; diff --git a/cli/flags.rs b/cli/flags.rs index 086b20e10f5c5f..f42dd771cc6815 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -133,7 +133,7 @@ pub struct Flags { pub allow_env: Option>, pub allow_hrtime: bool, pub allow_net: Option>, - pub allow_plugin: bool, + pub allow_ffi: Option>, pub allow_read: Option>, pub allow_run: Option>, pub allow_write: Option>, @@ -235,8 +235,15 @@ impl Flags { _ => {} } - if self.allow_plugin { - args.push("--allow-plugin".to_string()); + match &self.allow_ffi { + Some(ffi_allowlist) if ffi_allowlist.is_empty() => { + args.push("--allow-ffi".to_string()); + } + Some(ffi_allowlist) => { + let s = format!("--allow-ffi={}", ffi_allowlist.join(",")); + args.push(s); + } + _ => {} } if self.allow_hrtime { @@ -253,7 +260,7 @@ impl From for PermissionsOptions { allow_env: flags.allow_env, allow_hrtime: flags.allow_hrtime, allow_net: flags.allow_net, - allow_plugin: flags.allow_plugin, + allow_ffi: flags.allow_ffi, allow_read: flags.allow_read, allow_run: flags.allow_run, allow_write: flags.allow_write, @@ -1228,9 +1235,13 @@ fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .help("Allow running subprocesses"), ) .arg( - Arg::with_name("allow-plugin") - .long("allow-plugin") - .help("Allow loading plugins"), + Arg::with_name("allow-ffi") + .long("allow-ffi") + .min_values(0) + .takes_value(true) + .use_delimiter(true) + .require_equals(true) + .help("Allow loading dynamic libraries"), ) .arg( Arg::with_name("allow-hrtime") @@ -1577,7 +1588,7 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_run = Some(vec![]); flags.allow_read = Some(vec![]); flags.allow_write = Some(vec![]); - flags.allow_plugin = true; + flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; // TODO(@satyarohith): remove this flag in 2.0. let as_typescript = matches.is_present("ts"); @@ -1696,7 +1707,7 @@ fn repl_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_run = Some(vec![]); flags.allow_read = Some(vec![]); flags.allow_write = Some(vec![]); - flags.allow_plugin = true; + flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; } @@ -1876,9 +1887,12 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { debug!("run allowlist: {:#?}", &flags.allow_run); } - if matches.is_present("allow-plugin") { - flags.allow_plugin = true; + if let Some(ffi_wl) = matches.values_of("allow-ffi") { + let ffi_allowlist: Vec = ffi_wl.map(ToString::to_string).collect(); + flags.allow_ffi = Some(ffi_allowlist); + debug!("ffi allowlist: {:#?}", &flags.allow_ffi); } + if matches.is_present("allow-hrtime") { flags.allow_hrtime = true; } @@ -1888,7 +1902,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_net = Some(vec![]); flags.allow_run = Some(vec![]); flags.allow_write = Some(vec![]); - flags.allow_plugin = true; + flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; } if matches.is_present("prompt") { @@ -2227,7 +2241,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2564,7 +2578,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2587,7 +2601,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2611,7 +2625,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2648,7 +2662,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2678,7 +2692,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2698,7 +2712,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } @@ -2732,7 +2746,7 @@ mod tests { allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), - allow_plugin: true, + allow_ffi: Some(vec![]), allow_hrtime: true, ..Flags::default() } diff --git a/cli/standalone.rs b/cli/standalone.rs index 9a693d96133b5c..3c8dabd3a6d43c 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -195,7 +195,7 @@ fn metadata_to_flags(metadata: &Metadata) -> Flags { allow_env: permissions.allow_env, allow_hrtime: permissions.allow_hrtime, allow_net: permissions.allow_net, - allow_plugin: permissions.allow_plugin, + allow_ffi: permissions.allow_ffi, allow_read: permissions.allow_read, allow_run: permissions.allow_run, allow_write: permissions.allow_write, diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index e66b59c2b7327e..a27ebec4521524 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -502,7 +502,7 @@ fn lsp_hover_unstable_disabled() { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, - "text": "console.log(Deno.openPlugin);\n" + "text": "console.log(Deno.dlopen);\n" } }), ); @@ -537,7 +537,7 @@ fn lsp_hover_unstable_disabled() { }, "end": { "line": 0, - "character": 27 + "character": 23 } } })) @@ -555,7 +555,7 @@ fn lsp_hover_unstable_enabled() { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, - "text": "console.log(Deno.openPlugin);\n" + "text": "console.log(Deno.ppid);\n" } }), ); @@ -580,9 +580,9 @@ fn lsp_hover_unstable_enabled() { "contents":[ { "language":"typescript", - "value":"function Deno.openPlugin(filename: string): number" + "value":"const Deno.ppid: number" }, - "**UNSTABLE**: new API, yet to be vetted.\n\nOpen and initialize a plugin.\n\n```ts\nimport { assert } from \"https://deno.land/std/testing/asserts.ts\";\nconst rid = Deno.openPlugin(\"./path/to/some/plugin.so\");\n\n// The Deno.core namespace is needed to interact with plugins, but this is\n// internal so we use ts-ignore to skip type checking these calls.\n// @ts-ignore\nconst { op_test_sync, op_test_async } = Deno.core.ops();\n\nassert(op_test_sync);\nassert(op_test_async);\n\n// @ts-ignore\nconst result = Deno.core.opSync(\"op_test_sync\");\n\n// @ts-ignore\nconst result = await Deno.core.opAsync(\"op_test_sync\");\n```\n\nRequires `allow-plugin` permission.\n\nThe plugin system is not stable and will change in the future, hence the\nlack of docs. For now take a look at the example\nhttps://github.com/denoland/deno/tree/main/test_plugin" + "The pid of the current process's parent." ], "range":{ "start":{ @@ -591,7 +591,7 @@ fn lsp_hover_unstable_enabled() { }, "end":{ "line":0, - "character":27 + "character":21 } } })) diff --git a/cli/tests/test/allow_all.out b/cli/tests/test/allow_all.out index 3be26c6e0406db..9b7367b946059b 100644 --- a/cli/tests/test/allow_all.out +++ b/cli/tests/test/allow_all.out @@ -10,8 +10,8 @@ test env false ... ok [WILDCARD] test env true ... ok [WILDCARD] test run false ... ok [WILDCARD] test run true ... ok [WILDCARD] -test plugin false ... ok [WILDCARD] -test plugin true ... ok [WILDCARD] +test ffi false ... ok [WILDCARD] +test ffi true ... ok [WILDCARD] test hrtime false ... ok [WILDCARD] test hrtime true ... ok [WILDCARD] diff --git a/cli/tests/test/allow_all.ts b/cli/tests/test/allow_all.ts index e4e12144ee7508..b8f8c647d549d2 100644 --- a/cli/tests/test/allow_all.ts +++ b/cli/tests/test/allow_all.ts @@ -6,7 +6,7 @@ const permissions: Deno.PermissionName[] = [ "net", "env", "run", - "plugin", + "ffi", "hrtime", ]; diff --git a/cli/tests/test/allow_none.out b/cli/tests/test/allow_none.out index 96fb72278222d4..b79c7e6bf103c8 100644 --- a/cli/tests/test/allow_none.out +++ b/cli/tests/test/allow_none.out @@ -5,7 +5,7 @@ test write ... FAILED [WILDCARD] test net ... FAILED [WILDCARD] test env ... FAILED [WILDCARD] test run ... FAILED [WILDCARD] -test plugin ... FAILED [WILDCARD] +test ffi ... FAILED [WILDCARD] test hrtime ... FAILED [WILDCARD] failures: @@ -30,7 +30,7 @@ run PermissionDenied: Can't escalate parent thread permissions [WILDCARD] -plugin +ffi PermissionDenied: Can't escalate parent thread permissions [WILDCARD] @@ -45,7 +45,7 @@ failures: net env run - plugin + ffi hrtime test result: FAILED. 0 passed; 7 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD] diff --git a/cli/tests/test/allow_none.ts b/cli/tests/test/allow_none.ts index c0a930eb1210d4..7872c774e5033b 100644 --- a/cli/tests/test/allow_none.ts +++ b/cli/tests/test/allow_none.ts @@ -6,7 +6,7 @@ const permissions: Deno.PermissionName[] = [ "net", "env", "run", - "plugin", + "ffi", "hrtime", ]; diff --git a/cli/tests/test/ignore_permissions.ts b/cli/tests/test/ignore_permissions.ts index bd0567a460cd55..ff30844413d404 100644 --- a/cli/tests/test/ignore_permissions.ts +++ b/cli/tests/test/ignore_permissions.ts @@ -6,7 +6,7 @@ Deno.test({ net: true, env: true, run: true, - plugin: true, + ffi: true, hrtime: true, }, ignore: true, diff --git a/cli/tests/unit/test_util.ts b/cli/tests/unit/test_util.ts index 79e93d70d923c7..6745946c66d3e7 100644 --- a/cli/tests/unit/test_util.ts +++ b/cli/tests/unit/test_util.ts @@ -32,7 +32,7 @@ interface UnitTestPermissions { net?: boolean; env?: boolean; run?: boolean; - plugin?: boolean; + ffi?: boolean; hrtime?: boolean; } @@ -84,7 +84,7 @@ export function unitTest( net: false, env: false, run: false, - plugin: false, + ffi: false, hrtime: false, }, options.perms), }; diff --git a/cli/tests/workers/no_permissions_worker.js b/cli/tests/workers/no_permissions_worker.js index 8a4f79d57fb50f..db0d911ac9f952 100644 --- a/cli/tests/workers/no_permissions_worker.js +++ b/cli/tests/workers/no_permissions_worker.js @@ -1,14 +1,14 @@ self.onmessage = async () => { const hrtime = await Deno.permissions.query({ name: "hrtime" }); const net = await Deno.permissions.query({ name: "net" }); - const plugin = await Deno.permissions.query({ name: "plugin" }); + const ffi = await Deno.permissions.query({ name: "ffi" }); const read = await Deno.permissions.query({ name: "read" }); const run = await Deno.permissions.query({ name: "run" }); const write = await Deno.permissions.query({ name: "write" }); self.postMessage( hrtime.state === "denied" && net.state === "denied" && - plugin.state === "denied" && + ffi.state === "denied" && read.state === "denied" && run.state === "denied" && write.state === "denied", diff --git a/cli/tools/standalone.rs b/cli/tools/standalone.rs index dfec6c7dd2ffe6..5f89b592d8f134 100644 --- a/cli/tools/standalone.rs +++ b/cli/tools/standalone.rs @@ -201,7 +201,7 @@ pub fn compile_to_runtime_flags( allow_env: flags.allow_env, allow_hrtime: flags.allow_hrtime, allow_net: flags.allow_net, - allow_plugin: flags.allow_plugin, + allow_ffi: flags.allow_ffi, allow_read: flags.allow_read, allow_run: flags.allow_run, allow_write: flags.allow_write, diff --git a/extensions/ffi/00_ffi.js b/extensions/ffi/00_ffi.js new file mode 100644 index 00000000000000..3c4112a47333bb --- /dev/null +++ b/extensions/ffi/00_ffi.js @@ -0,0 +1,30 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const core = window.Deno.core; + + class DynamicLibrary { + #rid; + symbols = {}; + + constructor(path, symbols) { + this.#rid = core.opSync("op_ffi_load", { path, symbols }); + + for (const symbol in symbols) { + this.symbols[symbol] = (...parameters) => + core.opSync("op_ffi_call", { rid: this.#rid, symbol, parameters }); + } + } + + close() { + core.close(this.#rid); + } + } + + function dlopen(path, symbols) { + return new DynamicLibrary(path, symbols); + } + + window.__bootstrap.ffi = { dlopen }; +})(this); diff --git a/extensions/ffi/Cargo.toml b/extensions/ffi/Cargo.toml new file mode 100644 index 00000000000000..510b09292b7988 --- /dev/null +++ b/extensions/ffi/Cargo.toml @@ -0,0 +1,20 @@ +# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_ffi" +version = "0.1.0" +authors = ["the Deno authors"] +edition = "2018" +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" +description = "Dynamic library ffi for deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.95.0", path = "../../core" } +dlopen = "0.1.8" +libffi = { version = "0.0.3", package = "deno-libffi" } +serde = { version = "1.0.125", features = ["derive"] } diff --git a/extensions/ffi/README.md b/extensions/ffi/README.md new file mode 100644 index 00000000000000..cc2d81cd2fcdba --- /dev/null +++ b/extensions/ffi/README.md @@ -0,0 +1,3 @@ +# deno_ffi + +This crate implements dynamic library ffi. diff --git a/extensions/ffi/lib.rs b/extensions/ffi/lib.rs new file mode 100644 index 00000000000000..125e6da9945223 --- /dev/null +++ b/extensions/ffi/lib.rs @@ -0,0 +1,397 @@ +// Copyright 2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::include_js_files; +use deno_core::op_sync; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::Extension; +use deno_core::OpState; +use deno_core::Resource; +use deno_core::ResourceId; +use dlopen::raw::Library; +use libffi::middle::Arg; +use serde::Deserialize; +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::ffi::c_void; +use std::rc::Rc; + +pub struct Unstable(pub bool); + +fn check_unstable(state: &OpState, api_name: &str) { + let unstable = state.borrow::(); + + if !unstable.0 { + eprintln!( + "Unstable API '{}'. The --unstable flag must be provided.", + api_name + ); + std::process::exit(70); + } +} + +pub trait FfiPermissions { + fn check(&mut self, path: &str) -> Result<(), AnyError>; +} + +pub struct NoFfiPermissions; + +impl FfiPermissions for NoFfiPermissions { + fn check(&mut self, _path: &str) -> Result<(), AnyError> { + Ok(()) + } +} + +struct Symbol { + cif: libffi::middle::Cif, + ptr: libffi::middle::CodePtr, + parameter_types: Vec, + result_type: NativeType, +} + +struct DynamicLibraryResource { + lib: Library, + symbols: HashMap, +} + +impl Resource for DynamicLibraryResource { + fn name(&self) -> Cow { + "dynamicLibrary".into() + } + + fn close(self: Rc) { + drop(self) + } +} + +impl DynamicLibraryResource { + fn register( + &mut self, + symbol: String, + foreign_fn: ForeignFunction, + ) -> Result<(), AnyError> { + let fn_ptr = unsafe { self.lib.symbol::<*const c_void>(&symbol) }?; + let ptr = libffi::middle::CodePtr::from_ptr(fn_ptr as _); + let parameter_types = + foreign_fn.parameters.into_iter().map(NativeType::from); + let result_type = NativeType::from(foreign_fn.result); + let cif = libffi::middle::Cif::new( + parameter_types.clone().map(libffi::middle::Type::from), + result_type.into(), + ); + + self.symbols.insert( + symbol, + Symbol { + cif, + ptr, + parameter_types: parameter_types.collect(), + result_type, + }, + ); + + Ok(()) + } +} + +pub fn init(unstable: bool) -> Extension { + Extension::builder() + .js(include_js_files!( + prefix "deno:extensions/ffi", + "00_ffi.js", + )) + .ops(vec![ + ("op_ffi_load", op_sync(op_ffi_load::

)), + ("op_ffi_call", op_sync(op_ffi_call)), + ]) + .state(move |state| { + // Stolen from deno_webgpu, is there a better option? + state.put(Unstable(unstable)); + Ok(()) + }) + .build() +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +enum NativeType { + Void, + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + USize, + ISize, + F32, + F64, +} + +impl From for libffi::middle::Type { + fn from(native_type: NativeType) -> Self { + match native_type { + NativeType::Void => libffi::middle::Type::void(), + NativeType::U8 => libffi::middle::Type::u8(), + NativeType::I8 => libffi::middle::Type::i8(), + NativeType::U16 => libffi::middle::Type::u16(), + NativeType::I16 => libffi::middle::Type::i16(), + NativeType::U32 => libffi::middle::Type::u32(), + NativeType::I32 => libffi::middle::Type::i32(), + NativeType::U64 => libffi::middle::Type::u64(), + NativeType::I64 => libffi::middle::Type::i64(), + NativeType::USize => libffi::middle::Type::usize(), + NativeType::ISize => libffi::middle::Type::isize(), + NativeType::F32 => libffi::middle::Type::f32(), + NativeType::F64 => libffi::middle::Type::f64(), + } + } +} + +impl From for NativeType { + fn from(string: String) -> Self { + match string.as_str() { + "void" => NativeType::Void, + "u8" => NativeType::U8, + "i8" => NativeType::I8, + "u16" => NativeType::U16, + "i16" => NativeType::I16, + "u32" => NativeType::U32, + "i32" => NativeType::I32, + "u64" => NativeType::U64, + "i64" => NativeType::I64, + "usize" => NativeType::USize, + "isize" => NativeType::ISize, + "f32" => NativeType::F32, + "f64" => NativeType::F64, + _ => unimplemented!(), + } + } +} + +#[repr(C)] +union NativeValue { + void_value: (), + u8_value: u8, + i8_value: i8, + u16_value: u16, + i16_value: i16, + u32_value: u32, + i32_value: i32, + u64_value: u64, + i64_value: i64, + usize_value: usize, + isize_value: isize, + f32_value: f32, + f64_value: f64, +} + +impl NativeValue { + fn new(native_type: NativeType, value: Value) -> Self { + match native_type { + NativeType::Void => Self { void_value: () }, + NativeType::U8 => Self { + u8_value: value_as_uint::(value), + }, + NativeType::I8 => Self { + i8_value: value_as_int::(value), + }, + NativeType::U16 => Self { + u16_value: value_as_uint::(value), + }, + NativeType::I16 => Self { + i16_value: value_as_int::(value), + }, + NativeType::U32 => Self { + u32_value: value_as_uint::(value), + }, + NativeType::I32 => Self { + i32_value: value_as_int::(value), + }, + NativeType::U64 => Self { + u64_value: value_as_uint::(value), + }, + NativeType::I64 => Self { + i64_value: value_as_int::(value), + }, + NativeType::USize => Self { + usize_value: value_as_uint::(value), + }, + NativeType::ISize => Self { + isize_value: value_as_int::(value), + }, + NativeType::F32 => Self { + f32_value: value_as_f32(value), + }, + NativeType::F64 => Self { + f64_value: value_as_f64(value), + }, + } + } + + unsafe fn as_arg(&self, native_type: NativeType) -> Arg { + match native_type { + NativeType::Void => Arg::new(&self.void_value), + NativeType::U8 => Arg::new(&self.u8_value), + NativeType::I8 => Arg::new(&self.i8_value), + NativeType::U16 => Arg::new(&self.u16_value), + NativeType::I16 => Arg::new(&self.i16_value), + NativeType::U32 => Arg::new(&self.u32_value), + NativeType::I32 => Arg::new(&self.i32_value), + NativeType::U64 => Arg::new(&self.u64_value), + NativeType::I64 => Arg::new(&self.i64_value), + NativeType::USize => Arg::new(&self.usize_value), + NativeType::ISize => Arg::new(&self.isize_value), + NativeType::F32 => Arg::new(&self.f32_value), + NativeType::F64 => Arg::new(&self.f64_value), + } + } +} + +fn value_as_uint>(value: Value) -> T { + value + .as_u64() + .and_then(|v| T::try_from(v).ok()) + .expect("Expected ffi arg value to be an unsigned integer") +} + +fn value_as_int>(value: Value) -> T { + value + .as_i64() + .and_then(|v| T::try_from(v).ok()) + .expect("Expected ffi arg value to be a signed integer") +} + +fn value_as_f32(value: Value) -> f32 { + value_as_f64(value) as f32 +} + +fn value_as_f64(value: Value) -> f64 { + value + .as_f64() + .expect("Expected ffi arg value to be a float") +} + +#[derive(Deserialize, Debug)] +struct ForeignFunction { + parameters: Vec, + result: String, +} + +#[derive(Deserialize, Debug)] +struct FfiLoadArgs { + path: String, + symbols: HashMap, +} + +fn op_ffi_load( + state: &mut deno_core::OpState, + args: FfiLoadArgs, + _: (), +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.dlopen"); + let permissions = state.borrow_mut::(); + permissions.check(&args.path)?; + + let lib = Library::open(args.path)?; + let mut resource = DynamicLibraryResource { + lib, + symbols: HashMap::new(), + }; + + for (symbol, foreign_fn) in args.symbols { + resource.register(symbol, foreign_fn)?; + } + + Ok(state.resource_table.add(resource)) +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct FfiCallArgs { + rid: ResourceId, + symbol: String, + parameters: Vec, +} + +fn op_ffi_call( + state: &mut deno_core::OpState, + args: FfiCallArgs, + _: (), +) -> Result { + let resource = state + .resource_table + .get::(args.rid) + .ok_or_else(bad_resource_id)?; + + let symbol = resource + .symbols + .get(&args.symbol) + .ok_or_else(bad_resource_id)?; + + let native_values = symbol + .parameter_types + .iter() + .zip(args.parameters.into_iter()) + .map(|(&native_type, value)| NativeValue::new(native_type, value)) + .collect::>(); + + let call_args = symbol + .parameter_types + .iter() + .zip(native_values.iter()) + .map(|(&native_type, native_value)| unsafe { + native_value.as_arg(native_type) + }) + .collect::>(); + + Ok(match symbol.result_type { + NativeType::Void => { + json!(unsafe { symbol.cif.call::<()>(symbol.ptr, &call_args) }) + } + NativeType::U8 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::I8 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::U16 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::I16 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::U32 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::I32 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::U64 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::I64 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::USize => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::ISize => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::F32 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + NativeType::F64 => { + json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) + } + }) +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 044c3a1a13a720..b2bad984487744 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -23,6 +23,7 @@ deno_console = { version = "0.13.0", path = "../extensions/console" } deno_core = { version = "0.95.0", path = "../core" } deno_crypto = { version = "0.27.0", path = "../extensions/crypto" } deno_fetch = { version = "0.36.0", path = "../extensions/fetch" } +deno_ffi = { version = "0.1.0", path = "../extensions/ffi" } deno_http = { version = "0.4.0", path = "../extensions/http" } deno_net = { version = "0.4.0", path = "../extensions/net" } deno_timers = { version = "0.11.0", path = "../extensions/timers" } @@ -43,6 +44,7 @@ deno_console = { version = "0.13.0", path = "../extensions/console" } deno_core = { version = "0.95.0", path = "../core" } deno_crypto = { version = "0.27.0", path = "../extensions/crypto" } deno_fetch = { version = "0.36.0", path = "../extensions/fetch" } +deno_ffi = { version = "0.1.0", path = "../extensions/ffi" } deno_http = { version = "0.4.0", path = "../extensions/http" } deno_net = { version = "0.4.0", path = "../extensions/net" } deno_timers = { version = "0.11.0", path = "../extensions/timers" } diff --git a/runtime/build.rs b/runtime/build.rs index 4e061c43868a43..bb7947f36ce804 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -60,6 +60,7 @@ fn create_runtime_snapshot(snapshot_path: &Path, files: Vec) { deno_broadcast_channel::InMemoryBroadcastChannel::default(), false, // No --unstable. ), + deno_ffi::init::(false), deno_net::init::(None, false), // No --unstable. deno_http::init(), ]; diff --git a/runtime/js/11_workers.js b/runtime/js/11_workers.js index b598326354d5eb..38267f5715b6dc 100644 --- a/runtime/js/11_workers.js +++ b/runtime/js/11_workers.js @@ -119,7 +119,7 @@ env = "inherit", hrtime = "inherit", net = "inherit", - plugin = "inherit", + ffi = "inherit", read = "inherit", run = "inherit", write = "inherit", @@ -128,7 +128,7 @@ env: parseUnitPermission(env, "env"), hrtime: parseUnitPermission(hrtime, "hrtime"), net: parseArrayPermission(net, "net"), - plugin: parseUnitPermission(plugin, "plugin"), + ffi: parseUnitPermission(ffi, "ffi"), read: parseArrayPermission(read, "read"), run: parseUnitPermission(run, "run"), write: parseArrayPermission(write, "write"), @@ -175,7 +175,7 @@ env: false, hrtime: false, net: false, - plugin: false, + ffi: false, read: false, run: false, write: false, diff --git a/runtime/js/40_permissions.js b/runtime/js/40_permissions.js index c84f8fde820d61..1b053f9386a0b8 100644 --- a/runtime/js/40_permissions.js +++ b/runtime/js/40_permissions.js @@ -28,14 +28,14 @@ * @property {PermissionStatus} status */ - /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "plugin" | "hrtime">} */ + /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "ffi" | "hrtime">} */ const permissionNames = [ "read", "write", "net", "env", "run", - "plugin", + "ffi", "hrtime", ]; diff --git a/runtime/js/40_plugins.js b/runtime/js/40_plugins.js deleted file mode 100644 index 0796fd5cee4a9c..00000000000000 --- a/runtime/js/40_plugins.js +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -"use strict"; - -((window) => { - const core = window.Deno.core; - - function openPlugin(filename) { - const rid = core.opSync("op_open_plugin", filename); - core.syncOpsCache(); - return rid; - } - - window.__bootstrap.plugins = { - openPlugin, - }; -})(this); diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index aee07eae7bcfa4..796361d7ac2184 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -110,7 +110,6 @@ Signal: __bootstrap.signals.Signal, SignalStream: __bootstrap.signals.SignalStream, emit: __bootstrap.compilerApi.emit, - openPlugin: __bootstrap.plugins.openPlugin, kill: __bootstrap.process.kill, setRaw: __bootstrap.tty.setRaw, consoleSize: __bootstrap.tty.consoleSize, @@ -136,5 +135,6 @@ HttpClient: __bootstrap.fetch.HttpClient, createHttpClient: __bootstrap.fetch.createHttpClient, http: __bootstrap.http, + dlopen: __bootstrap.ffi.dlopen, }; })(this); diff --git a/runtime/lib.rs b/runtime/lib.rs index 2358899d4aa9fe..d7aaa8eecd54f8 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -4,6 +4,7 @@ pub use deno_broadcast_channel; pub use deno_console; pub use deno_crypto; pub use deno_fetch; +pub use deno_ffi; pub use deno_http; pub use deno_net; pub use deno_timers; diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs index 82ccf05060efc3..e08ddd1c05f41d 100644 --- a/runtime/ops/mod.rs +++ b/runtime/ops/mod.rs @@ -6,7 +6,6 @@ pub mod http; pub mod io; pub mod os; pub mod permissions; -pub mod plugin; pub mod process; pub mod runtime; pub mod signal; diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs index 3395430e499ae5..d9f341633c39a8 100644 --- a/runtime/ops/permissions.rs +++ b/runtime/ops/permissions.rs @@ -28,6 +28,7 @@ pub struct PermissionArgs { host: Option, variable: Option, command: Option, + library: Option, } pub fn op_query_permission( @@ -49,7 +50,7 @@ pub fn op_query_permission( ), "env" => permissions.env.query(args.variable.as_deref()), "run" => permissions.run.query(args.command.as_deref()), - "plugin" => permissions.plugin.query(), + "ffi" => permissions.ffi.query(args.library.as_deref()), "hrtime" => permissions.hrtime.query(), n => { return Err(custom_error( @@ -80,7 +81,7 @@ pub fn op_revoke_permission( ), "env" => permissions.env.revoke(args.variable.as_deref()), "run" => permissions.run.revoke(args.command.as_deref()), - "plugin" => permissions.plugin.revoke(), + "ffi" => permissions.ffi.revoke(args.library.as_deref()), "hrtime" => permissions.hrtime.revoke(), n => { return Err(custom_error( @@ -111,7 +112,7 @@ pub fn op_request_permission( ), "env" => permissions.env.request(args.variable.as_deref()), "run" => permissions.run.request(args.command.as_deref()), - "plugin" => permissions.plugin.request(), + "ffi" => permissions.ffi.request(args.library.as_deref()), "hrtime" => permissions.hrtime.request(), n => { return Err(custom_error( diff --git a/runtime/ops/plugin.rs b/runtime/ops/plugin.rs deleted file mode 100644 index cc3bf93d5cac0c..00000000000000 --- a/runtime/ops/plugin.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -use crate::permissions::Permissions; -use deno_core::error::AnyError; -use deno_core::op_sync; -use deno_core::Extension; -use deno_core::OpState; -use deno_core::Resource; -use deno_core::ResourceId; -use dlopen::symbor::Library; -use log::debug; -use std::borrow::Cow; -use std::mem; -use std::path::PathBuf; -use std::rc::Rc; - -/// A default `init` function for plugins which mimics the way the internal -/// extensions are initalized. Plugins currently do not support all extension -/// features and are most likely not going to in the future. Currently only -/// `init_state` and `init_ops` are supported while `init_middleware` and `init_js` -/// are not. Currently the `PluginResource` does not support being closed due to -/// certain risks in unloading the dynamic library without unloading dependent -/// functions and resources. -pub type InitFn = fn() -> Extension; - -pub fn init() -> Extension { - Extension::builder() - .ops(vec![("op_open_plugin", op_sync(op_open_plugin))]) - .build() -} - -pub fn op_open_plugin( - state: &mut OpState, - filename: String, - _: (), -) -> Result { - let filename = PathBuf::from(&filename); - - super::check_unstable(state, "Deno.openPlugin"); - let permissions = state.borrow_mut::(); - permissions.plugin.check()?; - - debug!("Loading Plugin: {:#?}", filename); - let plugin_lib = Library::open(filename).map(Rc::new)?; - let plugin_resource = PluginResource::new(&plugin_lib); - - // Forgets the plugin_lib value to prevent segfaults when the process exits - mem::forget(plugin_lib); - - let init = *unsafe { plugin_resource.0.symbol::("init") }?; - let rid = state.resource_table.add(plugin_resource); - let mut extension = init(); - - if !extension.init_js().is_empty() { - panic!("Plugins do not support loading js"); - } - - if extension.init_middleware().is_some() { - panic!("Plugins do not support middleware"); - } - - extension.init_state(state)?; - let ops = extension.init_ops().unwrap_or_default(); - for (name, opfn) in ops { - state.op_table.register_op(name, opfn); - } - - Ok(rid) -} - -struct PluginResource(Rc); - -impl Resource for PluginResource { - fn name(&self) -> Cow { - "plugin".into() - } - - fn close(self: Rc) { - unimplemented!(); - } -} - -impl PluginResource { - fn new(lib: &Rc) -> Self { - Self(lib.clone()) - } -} diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs index 2cd9a14ad2f859..5315ff5c745cab 100644 --- a/runtime/ops/worker_host.rs +++ b/runtime/ops/worker_host.rs @@ -3,6 +3,7 @@ use crate::permissions::resolve_read_allowlist; use crate::permissions::resolve_write_allowlist; use crate::permissions::EnvDescriptor; +use crate::permissions::FfiDescriptor; use crate::permissions::NetDescriptor; use crate::permissions::PermissionState; use crate::permissions::Permissions; @@ -218,6 +219,26 @@ fn merge_run_permission( Ok(main) } +fn merge_ffi_permission( + mut main: UnaryPermission, + worker: Option>, +) -> Result, AnyError> { + if let Some(worker) = worker { + if (worker.global_state < main.global_state) + || !worker.granted_list.iter().all(|x| main.check(&x.0).is_ok()) + { + return Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )); + } else { + main.global_state = worker.global_state; + main.granted_list = worker.granted_list; + } + } + Ok(main) +} + pub fn create_worker_permissions( main_perms: Permissions, worker_perms: PermissionsArg, @@ -226,7 +247,7 @@ pub fn create_worker_permissions( env: merge_env_permission(main_perms.env, worker_perms.env)?, hrtime: merge_boolean_permission(main_perms.hrtime, worker_perms.hrtime)?, net: merge_net_permission(main_perms.net, worker_perms.net)?, - plugin: merge_boolean_permission(main_perms.plugin, worker_perms.plugin)?, + ffi: merge_ffi_permission(main_perms.ffi, worker_perms.ffi)?, read: merge_read_permission(main_perms.read, worker_perms.read)?, run: merge_run_permission(main_perms.run, worker_perms.run)?, write: merge_write_permission(main_perms.write, worker_perms.write)?, @@ -241,8 +262,8 @@ pub struct PermissionsArg { hrtime: Option, #[serde(default, deserialize_with = "as_unary_net_permission")] net: Option>, - #[serde(default, deserialize_with = "as_permission_state")] - plugin: Option, + #[serde(default, deserialize_with = "as_unary_ffi_permission")] + ffi: Option>, #[serde(default, deserialize_with = "as_unary_read_permission")] read: Option>, #[serde(default, deserialize_with = "as_unary_run_permission")] @@ -414,6 +435,22 @@ where })) } +fn as_unary_ffi_permission<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let value: UnaryPermissionBase = + deserializer.deserialize_any(ParseBooleanOrStringVec)?; + + Ok(Some(UnaryPermission:: { + global_state: value.global_state, + granted_list: value.paths.into_iter().map(FfiDescriptor).collect(), + ..Default::default() + })) +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateWorkerArgs { diff --git a/runtime/permissions.rs b/runtime/permissions.rs index 5215743e3470f0..9e97ac234da7d5 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -202,6 +202,9 @@ pub struct EnvDescriptor(pub String); #[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize)] pub struct RunDescriptor(pub String); +#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize)] +pub struct FfiDescriptor(pub String); + impl UnaryPermission { pub fn query(&self, path: Option<&Path>) -> PermissionState { let path = path.map(|p| resolve_from_cwd(p).unwrap()); @@ -787,6 +790,104 @@ impl UnaryPermission { } } +impl UnaryPermission { + pub fn query(&self, lib: Option<&str>) -> PermissionState { + if self.global_state == PermissionState::Denied + && match lib { + None => true, + Some(lib) => self.denied_list.iter().any(|lib_| lib_.0 == lib), + } + { + PermissionState::Denied + } else if self.global_state == PermissionState::Granted + || match lib { + None => false, + Some(lib) => self.granted_list.iter().any(|lib_| lib_.0 == lib), + } + { + PermissionState::Granted + } else { + PermissionState::Prompt + } + } + + pub fn request(&mut self, lib: Option<&str>) -> PermissionState { + if let Some(lib) = lib { + let state = self.query(Some(lib)); + if state == PermissionState::Prompt { + if permission_prompt(&format!("ffi access to \"{}\"", lib)) { + self.granted_list.retain(|lib_| lib_.0 != lib); + self.granted_list.insert(FfiDescriptor(lib.to_string())); + PermissionState::Granted + } else { + self.denied_list.retain(|lib_| lib_.0 != lib); + self.denied_list.insert(FfiDescriptor(lib.to_string())); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + } else { + state + } + } else { + let state = self.query(None); + if state == PermissionState::Prompt { + if permission_prompt("ffi access") { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } else { + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + } else { + state + } + } + } + + pub fn revoke(&mut self, lib: Option<&str>) -> PermissionState { + if let Some(lib) = lib { + self.granted_list.retain(|lib_| lib_.0 != lib); + } else { + self.granted_list.clear(); + if self.global_state == PermissionState::Granted { + self.global_state = PermissionState::Prompt; + } + } + self.query(lib) + } + + pub fn check(&mut self, lib: &str) -> Result<(), AnyError> { + let (result, prompted) = self.query(Some(lib)).check( + self.name, + Some(&format!("\"{}\"", lib)), + self.prompt, + ); + if prompted { + if result.is_ok() { + self.granted_list.insert(FfiDescriptor(lib.to_string())); + } else { + self.denied_list.insert(FfiDescriptor(lib.to_string())); + self.global_state = PermissionState::Denied; + } + } + result + } + + pub fn check_all(&mut self) -> Result<(), AnyError> { + let (result, prompted) = + self.query(None).check(self.name, Some("all"), self.prompt); + if prompted { + if result.is_ok() { + self.global_state = PermissionState::Granted; + } else { + self.global_state = PermissionState::Denied; + } + } + result + } +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct Permissions { pub read: UnaryPermission, @@ -794,7 +895,7 @@ pub struct Permissions { pub net: UnaryPermission, pub env: UnaryPermission, pub run: UnaryPermission, - pub plugin: UnitPermission, + pub ffi: UnaryPermission, pub hrtime: UnitPermission, } @@ -803,7 +904,7 @@ pub struct PermissionsOptions { pub allow_env: Option>, pub allow_hrtime: bool, pub allow_net: Option>, - pub allow_plugin: bool, + pub allow_ffi: Option>, pub allow_read: Option>, pub allow_run: Option>, pub allow_write: Option>, @@ -904,8 +1005,21 @@ impl Permissions { } } - pub fn new_plugin(state: bool, prompt: bool) -> UnitPermission { - unit_permission_from_flag_bool(state, "plugin", "open a plugin", prompt) + pub fn new_ffi( + state: &Option>, + prompt: bool, + ) -> UnaryPermission { + UnaryPermission:: { + name: "ffi", + description: "load a dynamic library", + global_state: global_state_from_option(state), + granted_list: state + .as_ref() + .map(|v| v.iter().map(|x| FfiDescriptor(x.clone())).collect()) + .unwrap_or_else(HashSet::new), + denied_list: Default::default(), + prompt, + } } pub fn new_hrtime(state: bool, prompt: bool) -> UnitPermission { @@ -924,7 +1038,7 @@ impl Permissions { net: Permissions::new_net(&opts.allow_net, opts.prompt), env: Permissions::new_env(&opts.allow_env, opts.prompt), run: Permissions::new_run(&opts.allow_run, opts.prompt), - plugin: Permissions::new_plugin(opts.allow_plugin, opts.prompt), + ffi: Permissions::new_ffi(&opts.allow_ffi, opts.prompt), hrtime: Permissions::new_hrtime(opts.allow_hrtime, opts.prompt), } } @@ -936,7 +1050,7 @@ impl Permissions { net: Permissions::new_net(&Some(vec![]), false), env: Permissions::new_env(&Some(vec![]), false), run: Permissions::new_run(&Some(vec![]), false), - plugin: Permissions::new_plugin(true, false), + ffi: Permissions::new_ffi(&Some(vec![]), false), hrtime: Permissions::new_hrtime(true, false), } } @@ -1005,6 +1119,12 @@ impl deno_websocket::WebSocketPermissions for Permissions { } } +impl deno_ffi::FfiPermissions for Permissions { + fn check(&mut self, path: &str) -> Result<(), AnyError> { + self.ffi.check(path) + } +} + fn unit_permission_from_flag_bool( flag: bool, name: &'static str, @@ -1457,9 +1577,9 @@ mod tests { global_state: PermissionState::Prompt, ..Permissions::new_run(&Some(svec!["deno"]), false) }, - plugin: UnitPermission { - state: PermissionState::Prompt, - ..Default::default() + ffi: UnaryPermission { + global_state: PermissionState::Prompt, + ..Permissions::new_ffi(&Some(svec!["deno"]), false) }, hrtime: UnitPermission { state: PermissionState::Prompt, @@ -1490,8 +1610,10 @@ mod tests { assert_eq!(perms1.run.query(Some(&"deno".to_string())), PermissionState::Granted); assert_eq!(perms2.run.query(None), PermissionState::Prompt); assert_eq!(perms2.run.query(Some(&"deno".to_string())), PermissionState::Granted); - assert_eq!(perms1.plugin.query(), PermissionState::Granted); - assert_eq!(perms2.plugin.query(), PermissionState::Prompt); + assert_eq!(perms1.ffi.query(None), PermissionState::Granted); + assert_eq!(perms1.ffi.query(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms2.ffi.query(None), PermissionState::Prompt); + assert_eq!(perms2.ffi.query(Some(&"deno".to_string())), PermissionState::Granted); assert_eq!(perms1.hrtime.query(), PermissionState::Granted); assert_eq!(perms2.hrtime.query(), PermissionState::Prompt); }; @@ -1528,9 +1650,10 @@ mod tests { set_prompt_result(false); assert_eq!(perms.run.request(Some(&"deno".to_string())), PermissionState::Granted); set_prompt_result(true); - assert_eq!(perms.plugin.request(), PermissionState::Granted); + assert_eq!(perms.ffi.request(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms.ffi.query(None), PermissionState::Prompt); set_prompt_result(false); - assert_eq!(perms.plugin.request(), PermissionState::Granted); + assert_eq!(perms.ffi.request(Some(&"deno".to_string())), PermissionState::Granted); set_prompt_result(false); assert_eq!(perms.hrtime.request(), PermissionState::Denied); set_prompt_result(true); @@ -1561,9 +1684,9 @@ mod tests { global_state: PermissionState::Prompt, ..Permissions::new_run(&Some(svec!["deno"]), false) }, - plugin: UnitPermission { - state: PermissionState::Prompt, - ..Default::default() + ffi: UnaryPermission { + global_state: PermissionState::Prompt, + ..Permissions::new_ffi(&Some(svec!["deno"]), false) }, hrtime: UnitPermission { state: PermissionState::Denied, @@ -1582,7 +1705,7 @@ mod tests { assert_eq!(perms.net.revoke(Some(&("127.0.0.1", None))), PermissionState::Prompt); assert_eq!(perms.env.revoke(Some(&"HOME".to_string())), PermissionState::Prompt); assert_eq!(perms.run.revoke(Some(&"deno".to_string())), PermissionState::Prompt); - assert_eq!(perms.plugin.revoke(), PermissionState::Prompt); + assert_eq!(perms.ffi.revoke(Some(&"deno".to_string())), PermissionState::Prompt); assert_eq!(perms.hrtime.revoke(), PermissionState::Denied); }; } @@ -1595,7 +1718,7 @@ mod tests { net: Permissions::new_net(&None, true), env: Permissions::new_env(&None, true), run: Permissions::new_run(&None, true), - plugin: Permissions::new_plugin(false, true), + ffi: Permissions::new_ffi(&None, true), hrtime: Permissions::new_hrtime(false, true), }; @@ -1648,7 +1771,7 @@ mod tests { net: Permissions::new_net(&None, true), env: Permissions::new_env(&None, true), run: Permissions::new_run(&None, true), - plugin: Permissions::new_plugin(false, true), + ffi: Permissions::new_ffi(&None, true), hrtime: Permissions::new_hrtime(false, true), }; diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 773fce80f7c538..74e5fbafe20ee3 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -315,6 +315,8 @@ impl WebWorker { deno_crypto::init(options.seed), deno_webgpu::init(options.unstable), deno_timers::init::(), + // ffi + deno_ffi::init::(options.unstable), // Metrics metrics::init(), // Permissions ext (worker specific state) @@ -340,7 +342,6 @@ impl WebWorker { ), ops::os::init(), ops::permissions::init(), - ops::plugin::init(), ops::process::init(), ops::signal::init(), ops::tty::init(), diff --git a/runtime/worker.rs b/runtime/worker.rs index 94edd6f1e525ee..c64ef2baf8e2e2 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -115,6 +115,8 @@ impl MainWorker { ), deno_webgpu::init(options.unstable), deno_timers::init::(), + // ffi + deno_ffi::init::(options.unstable), // Metrics metrics::init(), // Runtime ops @@ -127,7 +129,6 @@ impl MainWorker { deno_net::init::(options.ca_data.clone(), options.unstable), ops::os::init(), ops::permissions::init(), - ops::plugin::init(), ops::process::init(), ops::signal::init(), ops::tty::init(), diff --git a/test_plugin/Cargo.toml b/test_ffi/Cargo.toml similarity index 66% rename from test_plugin/Cargo.toml rename to test_ffi/Cargo.toml index 53a94c4736dc38..737efe21e5bd9b 100644 --- a/test_plugin/Cargo.toml +++ b/test_ffi/Cargo.toml @@ -1,8 +1,8 @@ # Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. [package] -name = "test_plugin" -version = "0.0.1" +name = "test_ffi" +version = "0.1.0" authors = ["the deno authors"] edition = "2018" publish = false @@ -10,10 +10,5 @@ publish = false [lib] crate-type = ["cdylib"] -[dependencies] -deno_core = { path = "../core" } -futures = "0.3.15" -serde = "1" - [dev-dependencies] test_util = { path = "../test_util" } diff --git a/test_ffi/README.md b/test_ffi/README.md new file mode 100644 index 00000000000000..685385e4f718ac --- /dev/null +++ b/test_ffi/README.md @@ -0,0 +1 @@ +# `test_ffi` crate diff --git a/test_ffi/src/lib.rs b/test_ffi/src/lib.rs new file mode 100644 index 00000000000000..d9e950d4f664ea --- /dev/null +++ b/test_ffi/src/lib.rs @@ -0,0 +1,9 @@ +#[no_mangle] +pub extern "C" fn print_something() { + println!("something"); +} + +#[no_mangle] +pub extern "C" fn add(a: u32, b: u32) -> u32 { + a + b +} diff --git a/test_plugin/tests/integration_tests.rs b/test_ffi/tests/integration_tests.rs similarity index 59% rename from test_plugin/tests/integration_tests.rs rename to test_ffi/tests/integration_tests.rs index e408f59db168d5..7aa8a061142151 100644 --- a/test_plugin/tests/integration_tests.rs +++ b/test_ffi/tests/integration_tests.rs @@ -13,7 +13,7 @@ const BUILD_VARIANT: &str = "release"; fn basic() { let mut build_plugin_base = Command::new("cargo"); let mut build_plugin = - build_plugin_base.arg("build").arg("-p").arg("test_plugin"); + build_plugin_base.arg("build").arg("-p").arg("test_ffi"); if BUILD_VARIANT == "release" { build_plugin = build_plugin.arg("--release"); } @@ -21,10 +21,11 @@ fn basic() { assert!(build_plugin_output.status.success()); let output = deno_cmd() .arg("run") - .arg("--allow-plugin") + .arg("--allow-ffi") + .arg("--allow-read") .arg("--unstable") .arg("tests/test.js") - .arg(BUILD_VARIANT) + .env("NO_COLOR", "1") .output() .unwrap(); let stdout = std::str::from_utf8(&output.stdout).unwrap(); @@ -36,23 +37,9 @@ fn basic() { println!("{:?}", output.status); assert!(output.status.success()); let expected = "\ - Plugin rid: 3\n\ - Hello from sync plugin op.\n\ - args: TestArgs { val: \"1\" }\n\ - zero_copy: test\n\ - op_test_sync returned: test\n\ - Hello from async plugin op.\n\ - args: TestArgs { val: \"1\" }\n\ - zero_copy: 123\n\ - op_test_async returned: test\n\ - Hello from resource_table.add plugin op.\n\ - TestResource rid: 4\n\ - Hello from resource_table.get plugin op.\n\ - TestResource get value: hello plugin!\n\ - Hello from sync plugin op.\n\ - args: TestArgs { val: \"1\" }\n\ - Ops completed count is correct!\n\ - Ops dispatched count is correct!\n"; + something\n\ + 579\n\ + Correct number of resources\n"; assert_eq!(stdout, expected); assert_eq!(stderr, ""); } diff --git a/test_ffi/tests/test.js b/test_ffi/tests/test.js new file mode 100644 index 00000000000000..a7b0aba6db6752 --- /dev/null +++ b/test_ffi/tests/test.js @@ -0,0 +1,33 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +const targetDir = Deno.execPath().replace(/[^\/\\]+$/, ""); +const [libPrefix, libSuffix] = { + darwin: ["lib", "dylib"], + linux: ["lib", "so"], + windows: ["", "dll"], +}[Deno.build.os]; +const libPath = `${targetDir}/${libPrefix}test_ffi.${libSuffix}`; + +const resourcesPre = Deno.resources(); +const dylib = Deno.dlopen(libPath, { + "print_something": { parameters: [], result: "void" }, + "add": { parameters: ["u32", "u32"], result: "u32" }, +}); + +dylib.symbols.print_something(); +console.log(dylib.symbols.add(123, 456)); + +dylib.close(); +const resourcesPost = Deno.resources(); + +const preStr = JSON.stringify(resourcesPre, null, 2); +const postStr = JSON.stringify(resourcesPost, null, 2); +if (preStr !== postStr) { + throw new Error( + `Difference in open resources before dlopen and after closing: +Before: ${preStr} +After: ${postStr}`, + ); +} +console.log("Correct number of resources"); diff --git a/test_plugin/README.md b/test_plugin/README.md deleted file mode 100644 index b340389ce4b641..00000000000000 --- a/test_plugin/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `test_plugin` crate - -## To run this test manually - -``` -cd test_plugin - -../target/debug/deno run --unstable --allow-plugin tests/test.js debug -``` diff --git a/test_plugin/src/lib.rs b/test_plugin/src/lib.rs deleted file mode 100644 index 88761edcf1ef5d..00000000000000 --- a/test_plugin/src/lib.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -use std::borrow::Cow; -use std::cell::RefCell; -use std::rc::Rc; - -use deno_core::error::bad_resource_id; -use deno_core::error::AnyError; -use deno_core::op_async; -use deno_core::op_sync; -use deno_core::Extension; -use deno_core::OpState; -use deno_core::Resource; -use deno_core::ResourceId; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; - -#[no_mangle] -pub fn init() -> Extension { - Extension::builder() - .ops(vec![ - ("op_test_sync", op_sync(op_test_sync)), - ("op_test_async", op_async(op_test_async)), - ( - "op_test_resource_table_add", - op_sync(op_test_resource_table_add), - ), - ( - "op_test_resource_table_get", - op_sync(op_test_resource_table_get), - ), - ]) - .build() -} - -#[derive(Debug, Deserialize)] -struct TestArgs { - val: String, -} - -fn op_test_sync( - _state: &mut OpState, - args: TestArgs, - zero_copy: Option, -) -> Result { - println!("Hello from sync plugin op."); - - println!("args: {:?}", args); - - if let Some(buf) = zero_copy { - let buf_str = std::str::from_utf8(&buf[..])?; - println!("zero_copy: {}", buf_str); - } - - Ok("test".to_string()) -} - -async fn op_test_async( - _state: Rc>, - args: TestArgs, - zero_copy: Option, -) -> Result { - println!("Hello from async plugin op."); - - println!("args: {:?}", args); - - if let Some(buf) = zero_copy { - let buf_str = std::str::from_utf8(&buf[..])?; - println!("zero_copy: {}", buf_str); - } - - let (tx, rx) = futures::channel::oneshot::channel::>(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(1)); - tx.send(Ok(())).unwrap(); - }); - assert!(rx.await.is_ok()); - - Ok("test".to_string()) -} - -struct TestResource(String); -impl Resource for TestResource { - fn name(&self) -> Cow { - "TestResource".into() - } -} - -fn op_test_resource_table_add( - state: &mut OpState, - text: String, - _: (), -) -> Result { - println!("Hello from resource_table.add plugin op."); - - Ok(state.resource_table.add(TestResource(text))) -} - -fn op_test_resource_table_get( - state: &mut OpState, - rid: ResourceId, - _: (), -) -> Result { - println!("Hello from resource_table.get plugin op."); - - Ok( - state - .resource_table - .get::(rid) - .ok_or_else(bad_resource_id)? - .0 - .clone(), - ) -} diff --git a/test_plugin/tests/test.js b/test_plugin/tests/test.js deleted file mode 100644 index 2a2fa66b3aac41..00000000000000 --- a/test_plugin/tests/test.js +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -// deno-lint-ignore-file - -const filenameBase = "test_plugin"; - -let filenameSuffix = ".so"; -let filenamePrefix = "lib"; - -if (Deno.build.os === "windows") { - filenameSuffix = ".dll"; - filenamePrefix = ""; -} else if (Deno.build.os === "darwin") { - filenameSuffix = ".dylib"; -} - -const filename = `../target/${ - Deno.args[0] -}/${filenamePrefix}${filenameBase}${filenameSuffix}`; - -const resourcesPre = Deno.resources(); - -const pluginRid = Deno.openPlugin(filename); -console.log(`Plugin rid: ${pluginRid}`); - -const { - op_test_sync, - op_test_async, - op_test_resource_table_add, - op_test_resource_table_get, -} = Deno.core.ops(); - -if ( - op_test_sync === null || - op_test_async === null || - op_test_resource_table_add === null || - op_test_resource_table_get === null -) { - throw new Error("Not all expected ops were registered"); -} - -function runTestSync() { - const result = Deno.core.opSync( - "op_test_sync", - { val: "1" }, - new Uint8Array([116, 101, 115, 116]), - ); - - console.log(`op_test_sync returned: ${result}`); - - if (result !== "test") { - throw new Error("op_test_sync returned an unexpected value!"); - } -} - -async function runTestAsync() { - const promise = Deno.core.opAsync( - "op_test_async", - { val: "1" }, - new Uint8Array([49, 50, 51]), - ); - - if (!(promise instanceof Promise)) { - throw new Error("Expected promise!"); - } - - const result = await promise; - console.log(`op_test_async returned: ${result}`); - - if (result !== "test") { - throw new Error("op_test_async promise resolved to an unexpected value!"); - } -} - -function runTestResourceTable() { - const expect = "hello plugin!"; - - const testRid = Deno.core.opSync("op_test_resource_table_add", expect); - console.log(`TestResource rid: ${testRid}`); - - if (testRid === null || Deno.resources()[testRid] !== "TestResource") { - throw new Error("TestResource was not found!"); - } - - const testValue = Deno.core.opSync("op_test_resource_table_get", testRid); - console.log(`TestResource get value: ${testValue}`); - - if (testValue !== expect) { - throw new Error("Did not get correct resource value!"); - } - - Deno.close(testRid); -} - -function runTestOpCount() { - const start = Deno.metrics(); - - Deno.core.opSync("op_test_sync", { val: "1" }); - - const end = Deno.metrics(); - - if (end.opsCompleted - start.opsCompleted !== 1) { - throw new Error("The opsCompleted metric is not correct!"); - } - console.log("Ops completed count is correct!"); - - if (end.opsDispatched - start.opsDispatched !== 1) { - throw new Error("The opsDispatched metric is not correct!"); - } - console.log("Ops dispatched count is correct!"); -} - -function runTestPluginClose() { - // Closing does not yet work - Deno.close(pluginRid); - - const resourcesPost = Deno.resources(); - - const preStr = JSON.stringify(resourcesPre, null, 2); - const postStr = JSON.stringify(resourcesPost, null, 2); - if (preStr !== postStr) { - throw new Error( - `Difference in open resources before openPlugin and after Plugin.close(): -Before: ${preStr} -After: ${postStr}`, - ); - } - console.log("Correct number of resources"); -} - -runTestSync(); -await runTestAsync(); -runTestResourceTable(); - -runTestOpCount(); -// runTestPluginClose();