Stream: rfc-notifications

Topic: rfcs / PR #39 add Wasmtime plugin RFC


view this post on Zulip RFC notifications bot (Oct 18 2024 at 15:31):

rvolosatovs opened PR #39 from rvolosatovs:wasmtime-plugins to bytecodealliance:main:

Refs https://github.com/bytecodealliance/wasmtime/issues/7348

Rendered

view this post on Zulip RFC notifications bot (Oct 18 2024 at 16:42):

programmerjake commented on PR #39:

i don't see a way to access a .so global variable from wasm...

view this post on Zulip RFC notifications bot (Oct 18 2024 at 17:46):

rvolosatovs commented on PR #39:

i don't see a way to access a .so global variable from wasm...

indeed, I did not add this functionality in the current wasi-dl draft, it's really just an approximation sufficient for a very basic PoC

Does https://github.com/rvolosatovs/wasi-dl/pull/1 address your concern? alloc can be interpreted as any ffi-type or even a function.

Perhaps resource symbol is redundant altogether and lookups should just return alloc resources, which can be interpreted as functions

view this post on Zulip RFC notifications bot (Oct 18 2024 at 17:50):

rvolosatovs edited a comment on PR #39:

i don't see a way to access a .so global variable from wasm...

indeed, I did not add this functionality in the current wasi-dl draft, it's really just an approximation sufficient for a very basic PoC

Does https://github.com/rvolosatovs/wasi-dl/pull/1 address your concern? alloc can be interpreted as any ffi-type or even a function.

Perhaps resource symbol is redundant altogether and lookups should just return alloc resources, which can be interpreted as functions

Edit: added https://github.com/rvolosatovs/wasi-dl/pull/1/commits/30ea77f0d5679f8382603890408c6fef0bc4713b

view this post on Zulip RFC notifications bot (Oct 18 2024 at 17:52):

rvolosatovs updated PR #39.

view this post on Zulip RFC notifications bot (Oct 18 2024 at 17:53):

rvolosatovs edited a comment on PR #39:

i don't see a way to access a .so global variable from wasm...

indeed, I did not add this functionality in the current wasi-dl draft, it's really just an approximation sufficient for a very basic PoC

Does https://github.com/rvolosatovs/wasi-dl/pull/1 address your concern? alloc can be interpreted as any ffi-type or even a function.

Perhaps resource symbol is redundant altogether and lookups should just return alloc resources, which can be interpreted as functions

Edit: added https://github.com/rvolosatovs/wasi-dl/pull/1/commits/30ea77f0d5679f8382603890408c6fef0bc4713b
Edit 2: updated PR with https://github.com/bytecodealliance/rfcs/pull/39/commits/b48bdaa743642403ab3ac771495bd882f571d88b

view this post on Zulip RFC notifications bot (Oct 18 2024 at 18:08):

programmerjake commented on PR #39:

Does rvolosatovs/wasi-dl#1 address your concern?

yes!

view this post on Zulip RFC notifications bot (Oct 18 2024 at 19:32):

rvolosatovs updated PR #39.

view this post on Zulip RFC notifications bot (Oct 18 2024 at 19:34):

rvolosatovs edited PR #39:

Refs https://github.com/bytecodealliance/wasmtime/issues/7348

Rendered

view this post on Zulip RFC notifications bot (Oct 18 2024 at 19:37):

rvolosatovs commented on PR #39:

Did a small update to ensure the symbol lookups are typed https://github.com/bytecodealliance/rfcs/pull/39/commits/b9ae4c912db5d96956c1b57a2e74696a55db6b62

view this post on Zulip RFC notifications bot (Oct 21 2024 at 16:15):

sunfishcode commented on PR #39:

Such interface is unsafe and it must be used with extreme care, however that is no different from any other host plugin, which would be loaded via dlopen.

There are two kinds of unsafe relevant here. One is whether the plugin code is unsafe, and I agree that this is basically the same with any host plugin system we'd design here. The other is whether Wasm code using the plugin code is unsafe.

The libffi-style approach in this proposal looks like it means that we'd additionally have to treat the Wasm that calls the code as unsafe by default, and while there are potential ways to make it safe, they aren't described here.

Also, the libffi-style approach in this proposal looks like it would mean that the Wasm would not be portable, in general, because libffi doesn't encapsulate all C ABI details. What is off_t a typedef for? What is the value of ENOENT? And so on.

[wasi-dl]-based approach also provides greater security, since the implementation of [wasi-dl] may restrict the set of libraries allowed to be loaded and potentially define the exact signatures for symbols defined in them.

This proposal does not currently describe how this would work. And, signatures alone would not be sufficient, because libffi-style bindings also include raw pointers.

Perhaps it would be possible to design an interface description language sophisticated enough to describe these interfaces, including signatures, lifetime information, synchronization information, and perhaps also resource lifetimes (eg. open files that need to be explicitly closed and not used thereafter), and perhaps eventually even a way to describe C unions, or some safe variant-like subset of them. If this rfc implies the design of a new interface description language, it'd be good to say more about what that looks like.

As an alternative to exposing wasi-dl interface to (plugin) components, we could use the dynamic libraries themselves as the host plugins. For that we would need to carefully design a set of conventions specific to wasmtime for such plugins to be able to define their exports and expose them to components.

Such an approach would require custom-built dynamic libraries for plugins, if an existing library was desired to be used, an "adapter" library would need to be built, which would in turn dynamically-load that library.

It looks like this proposal would also usually want "adapter" libraries too, or at least adapter layers, because I don't expect we'll want normal Wasm code talking directly to these low-level libffi-style APIs, for ergonomics, language-independence, portability, and potential security reasons. And these adapters are going to be tedious to write and maintain, because they need to be written for each source language that needs them, and they'll have a lot of repetitive low-level code. I imagine we'd pretty quickly find ourselves wanting bindings generators for this task.

And if we're going to design a language-independent sandboxable interface description language with tooling around it for generating bindings, we should think carefully about whether or not we already have one, and what relationships we want :smile:.

view this post on Zulip RFC notifications bot (Oct 31 2024 at 19:36):

fitzgen submitted PR review:

Thanks for writing up this RFC!

I agree that a plugin system geared towards allowing hosts to define and expose new capabilities to Wasm guests that Wasmtime has no builtin knowledge of is very valuable.

Unfortunately, I think a missing constraint is that we fundamentally cannot trust Wasm guests, so we can't just expose dlopen/dlsym and raw FFI types to them. Therefore, I don't think the solution proposed here is something we can pursue. More details inline below.

That said, I also sketch (very roughly) an alternative approach that should address the same motivations but which avoids giving untrusted Wasm guests raw dlopen powers.

view this post on Zulip RFC notifications bot (Oct 31 2024 at 19:36):

fitzgen created PR review comment:

I guess the answer to the previous question is "yes" then.

I see @sunfishcode's comments now, and I agree with the gist of his points.

There is a difference between whether

  1. the plugin internally is using unsafe but exposing a safe interface, and
  2. the plugin's interface is itself unsafe.

With (1) the (untrusted and potentially malicious) Wasm guest cannot trigger any memory safety, modulo implementation bugs in the plugin itself.

With (2) the (untrusted and potentially malicious) Wasm guest can trivially trigger memory unsafety. That is, (2) is handing security vulnerabilities to Wasm guests by design.

So (2) is a complete non-starter; it is contradictory to Wasmtime's (and the BA's) mission and values.

And -- correct me if I'm wrong! -- this RFC seems to be proposing (2) so, unless I am misunderstanding the proposal, this is not an approach we should consider or pursue any further.

view this post on Zulip RFC notifications bot (Oct 31 2024 at 19:36):

fitzgen created PR review comment:

To be more constructive, I would suggest an alternative approach that maintains a safe interface to Wasm, something like:

In the above sketch, the plugin.so is trusted, but the Wasm is not. Any unsafety can only come from bugs in the plugin.so (either from its internal implementation or if its functions' types don't match the WIT interface it claims). Notably, unsafety cannot originate from within (untrusted and potentially malicious) Wasm guests, no matter what garbage values they indirectly pass to plugin.so.

The tricky parts here will be:

view this post on Zulip RFC notifications bot (Oct 31 2024 at 19:36):

fitzgen created PR review comment:

Would this result in memory unsafety if the Wasm (which is untrusted, and potentially malicious) passes the wrong number or type of arguments and returns?

Or is it expected that Wasmtime will somehow dynamically check these calls?

Similar question for declaring FFI struct types and their fields.

view this post on Zulip RFC notifications bot (Nov 01 2024 at 12:14):

rvolosatovs commented on PR #39:

Thanks for the feedback @sunfishcode @fitzgen!

In general I feel that perhaps I misjudged the expected level of detail for RFCs in this repository, this RFC currently is very much a high-level idea/direction, as opposed to directly-implementable design document, which seems to what people are searching for here.

First, let's agree on some terms:

In this RFC by component composition I mostly refer to function-style composition, and not component composition as defined at https://component-model.bytecodealliance.org/creating-and-consuming/composing.html#what-is-composition
For example, wasi-virt can mostly fulfill the composition as would be required here.

More formally, let's assume that components are morphisms (functors) that map a set of interfaces (imports) to another set of interfaces (exports).

Their composition is depicted here: ![composition](https://upload.wikimedia.org/wikipedia/commons/e/ef/Commutative_diagram_for_morphism.svg), taken directly from Category theory Wikipedia page.

Here's an example in context of this RFC:

// Trusted Wasm targets this world
world plugin {
    // These two interfaces are provided by the host:
    import wasi:sockets/tcp;
    import wasi:dl/dl;

    // These two interfaces are provided to the guest:
    export wasi:sockets/tcp;
    export wasi:keyvalue/store;
}

// Untrusted Wasm targets this world
world guest {
    // These two interfaces are either directly provided by the plugin component or passed through to the host *staticaly* by the composition tool:
    import wasi:sockets/tcp;
    import wasi:keyvalue/store;

    export wasi:http/incoming-handler;

    // NOTE: This import would *not* be satisfied:
    // import wasi:dl/dl;
}

world composed {
    import wasi:sockets/tcp;
    import wasi:dl/dl;
    export wasi:http/incoming-handler;
}

@fitzgen you seem to imply that all Wasm is implicitly untrusted.

I'm not sure I agree with that statement and the assumption I'm operating upon is that whether a trusted piece of code is compiled into a native application/library or a Wasm component should not change the "trustworthiness" of the produced artifact. That's a key assumption on which this RFC is built.

Is there something specific about Wasm components I'm not aware of, that would make them inherently untrusted?

In https://github.com/bytecodealliance/rfcs/pull/39#discussion_r1825062649 you've outlined a way how a plugin could be loaded by Wasmtime:

Note, that adding functions to the Linker using dlopen and instantiating the (untrusted) Wasm component using it produces a runtime object, which is effectively the composition as I defined above, except it happens at runtime.

In the context of this RFC, the plugin could operate exactly like you've outlined in https://github.com/bytecodealliance/rfcs/pull/39#discussion_r1825062649, except wasmtime CLI would load plugin.wasm as opposed to plugin.so.

Let's consider an example with a shared library plugin (this is not an API suggestion, just a quick example sketch):

wasmtime serve --plugin plugin.so untrusted.wasm

plugin.so would be operating directly as part of runtime's process with no sandboxing whatsoever, it has full, unconstrained access to the OS and runtime process memory.

An example usage with a Wasm component plugin could look like this:

wasmtime serve --plugin plugin.wasm -P tcp=y -P dl=y untrusted.wasm

In both cases, one way or another, wasmtime would need to produce a "runtime composition" of a plugin and the guest component.

Arguably, the Wasm plugin option is safer, since the runtime can control what libraries, symbols and their signatures can the plugin access.

In this RFC I've decided to start with a simple approach and give the wasmtime CLI user more control and produce such composition ahead-of-time, drasticaly reducing the scope for this feature and improving performance.

If (trusted) plugin.wasm (with optional wasi-dl access) was run in a separate sandbox, would that address your concerns @fitzgen?

There are two kinds of unsafe relevant here. One is whether the plugin code is unsafe, and I agree that this is basically the same with any host plugin system we'd design here. The other is whether Wasm code _using_ the plugin code is unsafe.

The libffi-style approach in this proposal looks like it means that we'd additionally have to treat the Wasm that calls the code as unsafe by default, and while there are potential ways to make it safe, they aren't described here.

From perspective of memory safety purely, if wasmtime loaded plugin.so, which exported wasi:keyvalue/store, each wasi:keyvalue/store interface call in the guest would be unsafe.

Whether we trust the plugin code or not, guest code directly or indirectly invoking a symbol loaded from a shared object will always be potentially memory unsafe.

Like I mentioned above, the runtime could limit wasi-dl access and potentially made aware of the symbols exported by the libraries (or even statically link to libraries at compilation time and expose them via wasi-dl abstraction).

Effectively, wasi-dl could be turned into a shared object introspection interface, which would be type-safe and could even verify contract constraints not directly expressable by C type system.

One potential strategy could be using value definitions or just functions (since recursive types are not currently allowed) to either process a C header file ahead-of-time or somehow else (e.g. manually) produce something roughly similar to:

(component
  (import "wasi:dl/ffi" (instance
     (export "primitive-type" (type $primitive_type (enum
        "c-char"
        "uint64-t"
        ;; etc..
     )))
     ;; etc...
  ))
  (import "wasi:dl/dll" (instance
     (export "function" (type $function (sub resource)))
     ;; etc...
  ))
  ;; using value definition
  (export "SOMECONST" (value $primitive_type (enum "uint64-t"))
  ;; using a function, returns the C type of the constant
  (export "SOMECONST" (func (result $primitive_type)))

  ;; returns a typed `wasi:dl/dll.function`
  (export "myfunc" (func (result $function)))
)

If the (trusted) plugin was a Wasm component, there'd be no need for any custom symbols or ABI - answers to most of these questions would be provided directly by the component model.

Perhaps it would be possible to design an interface description language sophisticated enough to describe these interfaces, including signatures, lifetime information
[message truncated]

view this post on Zulip RFC notifications bot (Nov 01 2024 at 12:48):

bjorn3 commented on PR #39:

I think having adapter.so directly provide the wasm component interface rather than having to use an intermediate plugin.wasm is safer, faster and easier to use for the end user.

Plugin.wasm is effectively unsandboxed as any mistake in it's use of wasi-dl would cause UB. It is a lot easier to directly define a safe wasm component interface in adapter.so than to export an unsafe C api and then separately consume this C api in plugin.wasm and hope that you didn't accidentally cause an ABI mismatch (as soon as you use any non-fixed size integer type (or an integer type larger than the register size) or you use a struct type or enum in your C api, it becomes non-trivial to match the ABI unless you are the C compiler that compiled adapter.so. And if adapter.so is written in Rust, avoiding a separate plugin.wasm may enable the plugin writer to entirely avoid unsafe code.

Having the intermediate plugin.wasm also requires you to copy all data twice. Once from adapter.so to plugin.wasm and once from plugin.wasm to the wasm module that uses the plugin. If adapter.so directly provides a wasm component interface, it only needs to be copied once.

And finally it is easier for the end user if only adapter.so exists. This way there can't be a version mismatch between adapter.so and plugin.wasm (which will likely cause UB) and you only need to copy a single file around to use the plugin.

view this post on Zulip RFC notifications bot (Nov 01 2024 at 13:42):

rvolosatovs commented on PR #39:

Plugin.wasm is effectively unsandboxed as any mistake in it's use of wasi-dl would cause UB. It is a lot easier to directly define a safe wasm component interface in adapter.so than to export an unsafe C api and then separately consume this C api in plugin.wasm and hope that you didn't accidentally cause an ABI mismatch (as soon as you use any non-fixed size integer type (or an integer type larger than the register size) or you use a struct type or enum in your C api, it becomes non-trivial to match the ABI unless you are the C compiler that compiled adapter.so

I've outlined an example approach in https://github.com/bytecodealliance/rfcs/pull/39#issuecomment-2451778905, which would let prevent UB in using wasi-dl.
All primitive types are read and written via resource methods in wasi-dl https://github.com/rvolosatovs/wasi-dl/blob/6d2000d92d96b0967eb5a7ead314a765b7f596e2/wit/dl.wit#L68-L108, so a size mismatch is not possible for non-fixed size integers - the runtime knows the sizes of C primitives at compile time, and if the component would try to write 16-bit char using a u32, it would get an error from set-u32
The component can query the primitive sizes at runtime using sizeof: https://github.com/rvolosatovs/wasi-dl/blob/6d2000d92d96b0967eb5a7ead314a765b7f596e2/wit/dl.wit#L124

Structs are also fully supported by libffi: https://www.chiark.greenend.org.uk/doc/libffi-dev/html/Size-and-Alignment.html
Components would read and write them using resources https://github.com/rvolosatovs/wasi-dl/blob/6d2000d92d96b0967eb5a7ead314a765b7f596e2/wit/dl.wit#L88-L117, where the runtime takes care of the alignment and size.

Having the intermediate plugin.wasm also requires you to copy all data twice. Once from adapter.so to plugin.wasm and once from plugin.wasm to the wasm module that uses the plugin. If adapter.so directly provides a wasm component interface, it only needs to be copied once.

A plugin.wasm would directly read data through the pointer from the shared object. That data would then need to be copied (assuming shared refs are not allowed) into the guest component's memory.

With a plugin.so, it depends:
If we want plugin.so to directly write into runtime's memory, the only single-copy approach I see is the following:

Otherwise, we'd still need two copies

view this post on Zulip RFC notifications bot (Nov 01 2024 at 13:51):

rvolosatovs closed without merge PR #39.

view this post on Zulip RFC notifications bot (Nov 01 2024 at 13:51):

rvolosatovs commented on PR #39:

Writing an RFC for dlopen-based plugins was never my intention, I've originally been working on an RFC for RPC-based plugins only, but after gathering some internal feedback and building a small PoC, decided to pivot to try and produce a unified plugin interface (the Wasm-component based one), which would cover all use cases. I personally do not have a use case for the dlopen-based plugins - RPC-based plugins being the only use case I'm after. Basing RPC-based plugins on shared libraries is certainly a non-starter for my use case.

Given that it does not appear that Wasm-based Wasmtime plugins is something people are interested in at this time, I'll take a step back and just go ahead and close this PR, instead replacing it by my original proposal: https://github.com/bytecodealliance/rfcs/pull/40

view this post on Zulip RFC notifications bot (Nov 01 2024 at 13:55):

bjorn3 commented on PR #39:

I've outlined an example approach in https://github.com/bytecodealliance/rfcs/pull/39#issuecomment-2451778905, which would let prevent UB in using wasi-dl.
All primitive types are read and written via resource methods in wasi-dl https://github.com/rvolosatovs/wasi-dl/blob/6d2000d92d96b0967eb5a7ead314a765b7f596e2/wit/dl.wit#L68-L108, so a size mismatch is not possible for non-fixed size integers - the runtime knows the sizes of C primitives at compile time, and if the component would try to write 16-bit char using a u32, it would get an error from set-u32
The component can query the primitive sizes at runtime using sizeof: https://github.com/rvolosatovs/wasi-dl/blob/6d2000d92d96b0967eb5a7ead314a765b7f596e2/wit/dl.wit#L124

How does Wasmtime know what type signature that adapter.so needs?

Structs are also fully supported by libffi: https://www.chiark.greenend.org.uk/doc/libffi-dev/html/Size-and-Alignment.html

There are edge cases where even two C compilers for the same platform disagree on the right ABI. Libffi can not know which ABI to use in those cases.


Given that it does not appear that Wasm-based Wasmtime plugins is something people are interested in at this time, I'll take a step back and just go ahead and close this PR, instead replacing it by my original proposal: https://github.com/bytecodealliance/rfcs/pull/40

I personally would still love to see dylib based plugins that directly interface with wasm interface types, but RPC based plugins are also nice. While they would almost certainly be a bit slower, they would be easier to support for other wasm engines that can't support dlopen and would be much easier to sandbox at an OS level.


Last updated: Nov 22 2024 at 17:03 UTC