Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: how to distribute native plugins gracefully #4222

Closed
manyuanrong opened this issue Mar 2, 2020 · 24 comments
Closed

Discussion: how to distribute native plugins gracefully #4222

manyuanrong opened this issue Mar 2, 2020 · 24 comments
Labels

Comments

@manyuanrong
Copy link
Contributor

I think the native plugin is very important and urgent for deno, but it doesn't seem to pay much attention.

I'm using rust to develop a mongodb driver for deno, but I have a headache about how to distribute my binaries. Because native plugins cannot run well across platforms, they need to be compiled into binary targets for different platforms. These files are very large. We need to come up with some mechanism to distribute these files gracefully.

So I wrote https://github.com/manyuanrong/deno-plugin-prepare to simplify the work, but I don't want to go in this direction, that seems to lead back to package.json

I think the following aspects need to be considered:

  1. Avoid loading binary versions of all platforms, they should be selectively loaded according to the operating environment.

  2. Considering the cost of regular developers, all native plugin dependencies of an application should have some mechanism for preloading, which facilitates application deployment and speeds up running time. Similar to deno fetch, achieve the effect of npm install.

  3. Because the native plugin is not a first-class citizen, it cannot be loaded directly through import like ts/js/json. Currently, it can only be downloaded through the fetch api at runtime. These binary files are very large. Downloading them takes a lot of time This is difficult to use for production.

  4. npm tool solves these problems in engineering practice, but this is not consistent with deno's idea.

For all these reasons, I think the better way is to make the native plugin a first-class citizen.
Develop a manifest file format, such as .dnm (deno native manifest), mimeapplication/x-deno-native-manifest
Structure is similar:

{
 "types": "./plugin.d.ts",
 "targets": {
   "win": {
      "path": "../path/plugin.dll",
      "checksum": "1bc29b36f623ba82aaf6724fd3b16718"
    },
    "mac": {
      "path": "../path/plugin.dylib",
      "checksum": "1bc29b36f623ba82aaf6724fd3b16718"
    },
    "linux": {
      "path": "../path/plugin.so",
      "checksum": "1bc29b36f623ba82aaf6724fd3b16718"
    }
  }
}

Useimport directly in the program

// example.ts

import plugin_one from "https://deno.land/x/plugin_one/mod.dnm";
const plugin_two = await import("https://deno.land/x/plugin_two/mod.dnm");

Prefetching with deno fetch

deno fetch example.ts
@lucacasonato
Copy link
Member

I do like the idea of the manifest, because it is very clear what is happening - there is very little magical resolution going on there. I think though that just being able to (dynamically) import .so, .dylib and .dll would also solve the issue. The main issue right now is that plugins need to manually fetched and cached, which is not great because deno already has the infrastructure to fetch and cache files (and we already use it for non script files like wasm).

The manifest has the upside that it can be statically analyzed and can be displayed in deno info (would also work with static import of .so ect. files).

I am personally for the (dynamic) import of .so, .dylib and .dll files because that is not at all magical and would be reasonably easy to implement. X-TypeScript-Types would also be compatible with that, whereas in the manifest option that would not work the same.

Ref #3448

@ry
Copy link
Member

ry commented Mar 2, 2020

I think the native plugin is very important and urgent for deno, but it doesn't seem to pay much attention.

It's not that it's not getting enough attention - @afinch7 has been working quite hard on it. It's more that I am purposely slowing down plugin API until we have some refactoring complete in core. The form for the plugin system as it is now is incomplete. Before addressing (valid) ergonomic issues as you outline above, we need to first address the issues of resource table in plugins and figuring out how "op crates" work.

@crabmusket
Copy link
Contributor

crabmusket commented Mar 3, 2020

I'm keen to see where this goes. I'm currently developing a wrapper to allow Deno programs to use rusqlite.

I don't like the idea of making import behave in ways that it wouldn't in the browser. (It should be noted that importing .wasm files in the browser is a current proposal.) Does the Deno team have an opinion on doing static analysis of Deno namespace calls? If there were a Deno.resolvePlugin function that worked the same way as @manyuanrong's dnm manifest, could a command like deno fetch-plugins analyse the source for calls to resolvePlugin and fetch the necessary binaries? (Alternatively, deno fetch could just do this.)

It sort of begs the question: how are people deploying Deno code to production? If it's e.g. via Docker container than this is probably a moot point, as the correct plugin can be baked into the image. What's the deployment case that's being solved for with an idea like importing a dnm file?


I'm not sure if this is a separate discussion, but the security model seems to mean that --allow-plugin is the same as giving a script all permissions, because the plugin could perform arbitrary operations. Two mitigating ideas come to mind:

  1. Could it be possible to use a whitelist strategy in allow-plugin similar to allow-read? At least then only approved plugins could be loaded. I'm not sure if Deno.openPlugin already obeys filesystem read whitelisting.

  2. In a hypothetical per-module permission system (as people often seem to want), we could take advantage of the fact that an object of type Deno.Plugin can be passed around. A script written by first-party developers could be responsible for opening the plugin (and ensuring the plugin is loaded in a trusted way), then third-party code could be passed an instance of the plugin. I'm not sure if this helps, but it at least puts control of which plugins are loaded into the hands of the first-party.

@ry
Copy link
Member

ry commented Mar 3, 2020

Does the Deno team have an opinion on doing static analysis of Deno namespace calls?

We haven't discussed it - but in general I would shy away from such magic...

the security model seems to mean that --allow-plugin is the same as giving a script all permissions

I'd rather make this explicit by requiring --allow-all to use a plugin...

@nayeemrmn
Copy link
Collaborator

I'd rather make this explicit by requiring --allow-all to use a plugin...

Ref #3378, the argument against is the implications of being able to whitelist plugins.

@dyedgreen
Copy link
Contributor

With regards to security, it might also make sense to consider using something like WASI as the primary way to distribute 'native'-like modules.

This has benefits with regards to security and portability. Although it might come with a performance penalty in some cases. (Although one might argue that in these cases using a JS run-time is not necessarily the best choice for the problem anyways).

@KSXGitHub
Copy link
Contributor

@ry

I'd rather make this explicit by requiring --allow-all to use a plugin

I would consider plugin as an extension of Deno itself (rather than another module to be import), and as such, should have the same privilege as Deno. Decision the use plugin should be end-users' responsibility, not library makers'. Plugins should have their own --allow-* flags as well.

I have no idea what plugin should do in the mind of everyone here. But I can imagine a database plugin that allow JavaScript code to access SQL tables, this plugin would have its own permission flag correlate to database permissions (such as --sql:allow-read-table=TABLE_NAME). I can also imagine a compiler plugin that compiles Rust code to WASM bindgen for TypeScript code to use, this plugin would need no permission flag as it merely compile.

@ry
Copy link
Member

ry commented Mar 6, 2020

@KSXGitHub We can't enforce permissions for native code. Once you load a .so file, all security promises are gone.

@KSXGitHub
Copy link
Contributor

KSXGitHub commented Mar 6, 2020

@ry Indeed, that's why I said

I would consider plugin as an extension of Deno itself (rather than another module to be import), and as such, should have the same privilege as Deno

What I meant is that plugins should have their own security flags the same way Deno has.

@ry
Copy link
Member

ry commented Mar 6, 2020

@KSXGitHub but what use are security flags if the code is untrusted?

@KSXGitHub
Copy link
Contributor

@ry Security flags not because I don't trust the plugin, but because I don't trust third party code using said plugin. Just like I trust Deno, but not the random code I wish Deno to execute.

@crabmusket
Copy link
Contributor

crabmusket commented Mar 9, 2020

@KSXGitHub you're saying untrusted code could use a trusted plugin if the plugin promised to obey certain security features? That does make a lot of sense. For my use-case, I am trusting my sqlite plugin to only be able to perform operations on Sqlite databases - and I could even lock that down to only in-memory databases. So I'd be fine passing that plugin to any random piece of code from the net, knowing it couldn't use it to do anything other than create in-memory databases.

I think that's an important thing to know, but it is still meaningless in the context of Deno's permissions API. Unless we had per-module permissions where I could write trusted code that opened a trusted plugin, then passed the trusted plugin instance to untrusted code.

EDIT: or, if the plugin API was not a runtime thing? Like if I could say "run Deno with this plugin with this configuration" (e.g. "the sqlite plugin with permission to open databases in-memory only"), then at runtime that plugin is simply available to all (untrusted) code. Here's a hypothetical:

deno --plugin ./libsqlite.so:sqlite --plugin-config '{"sqlite": {"only_memory": true}}' untrusted.js

untrusted.js

// Untrusted code cannot open an arbitrary .so file, only the plugins I've loaded on the CLI
let sqlite = Deno.openPlugin("sqlite");
// fake plugin API because I can't remember how to use it off the top of my head
sqlite.ops.openConnection({
  "path": ":memory:"
});
// this throws an error because of the way the sqlite plugin is configured:
sqlite.ops.openConnection({
  "path": "../haxors.db"
});

This seems like it would work well for well-known, trusted plugins, and allow end-users to maintain a tighter security profile. It'd also be great if plugins could hook into Deno's security configuration so that e.g. my Sqlite plugin could check the program's --allow-read and --allow-write and act accordingly.

I'm not sure if this gives too much of a false sense of security :/... it is distributing increased trust to the ecosystem rather than Deno core itself. Using the --plugin flag, even with a "trusted" plugin, might be seen as a security risk. Hard to know if this is a step up from --allow-all.

ry added a commit to ry/deno that referenced this issue Apr 19, 2020
- Removes unnecessary RwLock and Rc around the op registry table
- Preparation to move resource_table to deno_core::Isolate.
- Towards denoland#3453, denoland#4222
ry added a commit that referenced this issue Apr 20, 2020
- Removes unnecessary RwLock and Rc around the op registry table
- Preparation to move resource_table to deno_core::Isolate.
- Towards #3453, #4222
@ejsmith
Copy link

ejsmith commented Apr 20, 2020

I'm using rust to develop a mongodb driver for deno, but I have a headache about how to distribute my binaries. Because native plugins cannot run well across platforms, they need to be compiled into binary targets for different platforms. These files are very large. We need to come up with some mechanism to distribute these files gracefully.

Maybe a dumb question @manyuanrong, but why are you building a mondodb driver as a native plugin instead of in TypeScript / JavaScript so that it would just work cross platform without worrying about native code portability?

SASUKE40 pushed a commit to SASUKE40/deno that referenced this issue May 7, 2020
- Removes unnecessary RwLock and Rc around the op registry table
- Preparation to move resource_table to deno_core::Isolate.
- Towards denoland#3453, denoland#4222
@ghost
Copy link

ghost commented May 20, 2020

Tell me, why bother with distributing dynamic libraries? They are present in large numbers in any distributions and operating systems. All you need to interact with them is the built-in FFI and a text description of the library functions on the Deno side

@soplwang
Copy link

soplwang commented May 20, 2020

Tell me, why bother with distributing dynamic libraries? They are present in large numbers in any distributions and operating systems. All you need to interact with them is the built-in FFI and a text description of the library functions on the Deno side

Not bother with distributing dynamic libraries, just bother with distributing native Deno plugins like in this discussion (source: https://deno.land/x/mongo).

Use more native plugins means:

  1. More risks (on safety)
  2. Distribution issue like in this discussion
  3. Can't easily bundle into one binary and copy deployable

My opinions:

  1. Instead writes native plugin, use WebAssembly as possible to keep us safe (WebAssembly as an IL with JIT nativization), easy distribution and bundling; - maybe 90% cases
  2. For cases access C-binding libraries not performance critical, FFI with a text description should be ok for me. It's safe, easy distribution and bundling too; - may be 5% cases
  3. For cases can't use WebAssembly and FFI (Iike access CUDA, Tensorflow, OpenCL/GL, native GUIs (Cocoa, WinUI/WinRT, GTK+, etc.), other language interop (Rust, C++, Java, etc.), and WebRTC, ffmpeg, etc.), I can afford install native Deno plugins via brew/apt/choco/deno first for safety. - may be 5% cases

BTW:
When writing, I find LLVM bitcode solution is safe when limit it load from specific external file only instead instantiate it from byte code inline. I means:

const rid = Deno.openPlugin('./deno_plugins/llvmBitcodePlugin.deno');
const { testSync, testAsync } = Deno.core.ops();

Compare with FFI solution, LLVM bitcode plugins can:

  1. Immediate usable of toolchain support (easy compile native plugins into bitcode);
  2. Easy integration and no changes on current plugin architecture (still use deno_plugin_init);
  3. Native performance for access CUDA and OpenGL, etc.;
  4. Not hard for distribution (maybe deno install-plugin https://example.com/plugin.deno) or even open it directly (i.e., Deno.openPlugin('https://example.com/plugin.deno'));
  5. Still can bundle into one binary for easy deployment.

@afinch7 any ideas?

After a few tests, llvm ORC JIT engine seems too big and heavy for Deno (about ≥20MB on macOS), not profitable. And llvm bitcode is not portable cross platforms (macOS, linux and Windows), seems just portable cross arch (x86_64, arm, arm64).

Maybe just a Deno.openPlugin('https://example.com/deno/xxx-linux_x86-64.so') is sufficient.

@Ciantic
Copy link

Ciantic commented Jul 1, 2020

I think the best would be to make Deno.openPlugin take in URL, and allow Deno to cache the DLL/native libraries centrally just like other imports.

Current behavior of Deno.openPlugin can lead to difficulties in future. Because if the Deno.openPlugin takes in relative paths, the plugin authors may start to think that relying on cwd is a good idea, which is not with the Deno.

Right now there is some unofficial .deno_plugins directory provided by the plugin_prepare, and if the plugin authors start to assume that directory is the canonical location, then changing to centrally cached location will be difficult.

Edit

Centrally located native library cache, out of sight, is the best user experience. Right now if you want to run e.g. sass compilation quickly in some directory you get also directory of .deno_plugins. And that's probably not what I want when I just quickly used sass tool from URL.

@tjstebbing
Copy link

I'm confused about the safety arguments people are using against native plugins.. (especially related to these arguments that people should 'just use WebAssembly').. why should I trust the authors of Deno any more or less than the authors of a native plugin I may choose to use with Deno? Surely using native plugins is an opt-in decision that I'm able to make and the security implications of that are on me?

@nayeemrmn
Copy link
Collaborator

I haven't mentioned it here yet -- this issue would be solved by https://github.com/tc39/proposal-asset-references. It's the general problem of "consuming non-code resources which are relative to a remote dependency".

@crabmusket
Copy link
Contributor

@pomke I agree with you that ultimately a user of Deno has to understand the risks and make decisions themselves. But Deno can still make design decisions that are better or worse given that basic fact. For example, the situation when Deno launched was "if you load a plugin, it can do anything and you can't control it". This means that your security options are binary: either don't enable plugins and don't get the benefits, or enable plugins and all the security risks that may entail. There are a variety of ways that Deno could provide more options to its users to get some security, even if Deno obviously can't guarantee complete security. That was the spirit of my suggestion above.

@stale
Copy link

stale bot commented Jan 6, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jan 6, 2021
@stale stale bot closed this as completed Jan 13, 2021
@eliassjogreen
Copy link
Contributor

Should probably not be closed?

@lucacasonato
Copy link
Member

I'd say dupe of #3448.

@nayeemrmn
Copy link
Collaborator

IMO this falls under distribution of non-code assets in general. So #5987, https://github.com/tc39/proposal-asset-references, etc. #3448 seems to be about importing them I guess?

@josh-hemphill
Copy link
Contributor

Instead of tc39/proposal-asset-reference wouldn't proposal-import-assertions which is already stage 3 provide a possible path to a solution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests