Rust, wasm, JS, Chrome, sandboxing?

I have a Rust/wasm + JS app running in the Chrome browser. At this point, I think I'm using all the features (wasm, webgl, webworkers, ...), so feel free to suggest crazy ideas.

I am trying to figure out the best way to do sandboxing. Here is the generic problem:

For this single-page-wasm/webgl app I am building, I want to allow untrusted third parties to provide "extensions/customizations" -- and I would prefer to just give them a real programming language.

Options I have considered so far are :

  1. allow arbitrary JS code execution, but stuff them in a webworker, so they only postmessage to main app; however, not sure if webworkers can make ajax calls

  2. allow arbitrary wasm code execution, also stuffed in webworker -- can we lock this down more ? I'm not 100% sure, since we would not be running wasmtime or wasmer -- but directly using the browser's wasm VM -- can we lock that down at all ?

  3. pick some language with an interpreter implemented in JS

  4. pick some language with an interpreter in Rust (quite a few crates for this)

=====

Questions:

Q1: Are there other options I am not considering ?

Q2: Recommendations for any of the options 1-4 above are welcome.

=====

Pre-emptive: why would you want this ?

Say you have a simple editor, like a mini-vim or mini-emacs running in the browser on wasm/webgl. Say you want to allow third party extensions. Vim's solution would be 'vimscript', Emacs solution would be 'elisp'; I'm wondering what my options here are, and would prefer as much to leverage Rust or Chrome as possible instead of inventing a new language.

Wasm can't do anything other than allocating memory and calling functions you provide using the javascript api. If you make sure that the functions you provide to the wasm module can handle untrusted code calling them, you should have an effective sandbox.

Yes they can. Webworkers are not suitable for sandboxing.

2 Likes

Naive followup: web_sys crate can do all types of crazy things: start web workers, webrtc, websockets, ajax requests ...

Are you saying that

  1. wasm-in-Chrome do not, by default, get these functions

  2. these functions are provided in a way that can be removed ?

Extensions can't use wasm-bindgen (which web-sys uses), this crate requires generating javascript code from the wasm module for providing all this functionality. The javascript code for one wasm module can't even be used by another wasm module in many cases. When you call WebAssembly.instantiate() to instantiate a wasm module, you need to pass an imports object containing all functions the wasm module can call. Wasm-bindgen provides you with a javascript wrapper which instantiates modules for you with an import object containing all api's that the wasm module wants to use. This is not mandatory. Instead you can manually define functions providing just the functionality that you want to expose in javascript and pass just those functions in the import object and then use extern "C" { ... } in the wasm module to bind against this. Or you could use wit-bindgen for generating glue code that serializes and deserializes non-primitive types like strings or structs, while stll allowing you to manually specify the full interface that the wasm module can use.

3 Likes

I think this is starting to make sense. Are all of the following assumptions correct ?

  1. A 'minimal' wasm runtime has import-functions = {}

  2. The reason Rust compiled to wasm32 has access to web_sys is that something in the toolchain does import-functions = { ... stuff in web_sys }. Start new web worker, open websocket, open webrtc, open ajax, ... are all possible due to callbacks in import-functions.

  3. If we fire up a new wasm module, and set import-functions = {}, then the worst thing that wasm module can do is just burn CPU and grow the linear memory to 4GB and maybe do potential side channel information leaking via memory / cache timings.

1 Like

Yes

Pretty much. wasm-bindgen writes a javascript file which does this for you.

Ignoring browser bugs (which would be considered high priority by the respective browser vendor) this is indeed the case. Also spectre/meltdown are not possible for such a wasm module as you have to explicitly pass in some function to measure time to be able to leak information through a timing side channel.

2 Likes

I repeat myself...remember if you compile rust to wasm, on the javascript side, your importObject must have an "env" inside.

Also, I was painting walls yesterday so had plenty of daydreaming. Thinking maybe you can make a rust function that takes a string(u8 slice) with a message and two numbers. This would make it possible to send different messages to different modules, The function in the module could match on the string to decide what to do. And the numbers could point to location and len() of bytes in the shared memory.

Then your third party modules would all have the same function that you call from java so you can bind to it easily.

I do not see how this is useful / relevant.

Either (1) I am misunderstanding your post or (2) you are misunderstanding the original question.

LOL, I think "and" not "or". But I will try to explain myself and understand you better and I apologize if I make some assumptions about you and am wrong.
By reading your posts it looks like you wrote something pretty neat and probably over my skill level. You seem to be doing something web based and non-trivial. I think maybe you did not bother doing the bindings from javascript to rust manually because it would have been to much work. I think you used tools wasm-bindgen etc but did not fully understand what they were doing. And then this thread explained it a bit better and I think you now understand what it does.
So back a bit to the binding manually you may have never done that? And I am thinking to sand box things you want to only bind the 3rd party wasm to specific things. Not anything the 3rd party wants to bind to. So you will have to write some simple bindings in javascript to bind to the 3rd party code. The 3rd party wasm can only interact with shared memory, so you point to a space in shared memory and send that to the 3rd party, that module can access the specified shared memory. Then the 3rd party code will do something, but you do not know what it will do yet. The 3rd party can make a wasm module that can modify the memory you share. But since it will have functions that you do not know what they are you will have to bind to a function that you know. And maybe that function that has a known name in the 3rd party can read the string sent in to know what to do. Of course the third party will have to register the functions it has with string names in your java somehow.

It that confusing? Sorry.

On the other thing. "env' if you do have to bind wasm manually to rust and read the docs on mozilla, there are some wasm text explains and they show example javascript "importObject" with a {} block. But rust compiled to wasm puts an "env" in the importObject and that may trip you up if you didn't know.

I think there is a big misunderstanding here, I'm not sure where.

  1. 'bindings' or JS <-> Rust communication is not a problem. I just use #[wasm_bindgen], and it has been great so far.

  2. 'communication' also is not an issue. I have Rust/wasm on main thread talking to Rust/main in webworker.

So where is the problem then? I want userA to be able to run an untrusted snippet from 'Bob', and limit the damage Bob can do.

Hypothetically, if we were building a Facebook clone (I am not), userA should be able to install a 'plugin' for Bobs_embedded_fahrenheit_celsius_converter, and be safe knowing that the plugin does not have the power to add/delete friends or send msgs on behalf of userA.

To do this, we want sandboxing -- to be able to run (potentially) malicious code, and yet limit what the code can do. @bjorn3 's solution to this problem is basically: use wasm, provide (as import list) list of functions the wasm is allowed to call.

Communication has always been tough for me....Thanks for your patience.

I was thinking your extensions would have functionality that you needed to invoke and you might not know the extensions functionality yet? And maybe you might want to call functions inside the extension when something happens. And you might not have the name of those functions because they are extensions. So you could bind to one function and send in text commands to the extension. And I don't know how wasm-bindgen can bind to the extensions if they are 3rd party so thought you might have to define those binding by hand.