Idea: ic_cdk should let us run wasm in wasm

TL;DR
The wasmtime execution environment used to run all canisters should expose functionality accessible through ic_cdk to let us run wasm code from inside our wasm canisters.

I have the need to run untrusted user code in my canister, using sort of a plugin system. Currently I am running a JS runtime in the canister which allows me to run that untrusted code in a fully sandboxed secure environment. One drawback with that is that JS is quite inefficient, hitting the instruction limit is a real risk.

In another thread we discussed the possibility of running wasm compiled code in the canister, effectively running wasm in wasm. This is some orders of magnitude (6) more efficient than interpreting JS. But it is still like 15 times less efficient than original canister code since you have to run a wasm runtime inside your canister that is run in another wasm runtime.

See that thread here: I just hit the instruction limit! - #34 by ulan

Wouldn’t it be nice/awesome if the wasmtime runtime that runs the canister exposed some functions accessible through ic_cdk that would allow us to run wasm modules from within our canisters in the same runtime environment as the canister? Then those modules would run with the same efficiency in terms of instruction consumption as the canister itself.

Would this even be technically feasible?

Why run wasm in wasm? Running untrusted code easily from within a canister opens up a new universe of functionality!

  • DAO governance tools with user submitted voting modules
  • Programmable NFTs
  • Installable micro applications running in an onchain cloud based linux like environment
  • Games where users can contribute with interactive building blocks
  • …

Composability on the IC would be taken to the next level! Any use case where you can imagine a plugin or installable module could be created using the same efficient execution as the canister is using.

Imagine being able to do something like:

    let wasm = include_bytes!("awesome_thing.wasm");
    let engine = ic_cdk::wasm::Engine::default();
    let module = ic_cdk::wasm::Module::new(&engine, &wasm[..]).unwrap();
    let mut store = ic_cdk::wasm::Store::new(module.engine(), ());
    let mut linker = ic_cdk::wasm::Linker::new(&engine);
    let run = linket.get_typed_func::<(), i64>(&store, "run").unwrap();
    Ok(run.call(&mut store, ()).unwrap())
4 Likes

I wonder if the Wasm Component Model will elegantly solve this issue

3 Likes

I was hinting at wasm runtime inside canister a while back. Compiling the a wasm runtime to wasm is not trivial, perhaps not even feasable today.

But as part of the system API, would be great!

First thing that comes to mind is app stores / installable mini dapps etc.

I always wanted my own miniOS in a canister. Imagine your own canister with a frontend where all your IC dapps live

IMO that’s the true vision of a decentralized web

3 Likes

This idea is totally lit and in frontend development it is already known as micro-frontend architecture. This architecture allows you to fetch prebuilt modules so like separately hosted components and combine them on the fly into a complete layout. Since this concept is still part of web 2.0 it’s not possible to share state (database data) between third-party services/apps so micro-frontends are generally only possible in-house. This is because data is managed via companies’ self-built authentication systems (which is obviously natural), but sharing identity and state between third-party apps is impossible. The vision of a 3.0 web where we can share state and identity open up millions of possibilities, and that is why I’m fully into ICP. For me, this is a new era of the internet!

2 Likes

Using the wasmi runtime in a canister worked without issues. It just runs 6x less efficiently compared to the main wasmtime runtime.

Example here:

3 Likes

If the goal is to run untrusted user code in a sandbox, why not run it in a separate canister?

1 Like

In some cases that could definitely be an option. In other cases it would make things rather messy. Imagine a plugin system with many plugins.

  • Installing and managing the plugin canisters would quite quickly become quite complex, supplying them with cycles etc
  • Interacting with the plugins would require cross canister calls
  • The plugin being a canister means it is not fully sandboxed as it could do HTTPS outcalls etc

… I imagine there being more reasons why spinning up new canisters can be inefficient, above reasons where just the first three that popped up.

2 Likes

The amount of work that the replica would need to do in order to support running arbitrary Wasm would be similar to having a separate canister:

  • Since the plugin code is untrusted, it needs to run in a separate sandbox process. Otherwise, the plugin code could exploit a bug in Wasmtime to take control over the main canister.
  • The plugin code would need to be compiled before execution because Wasmtime is not an interpreter. Since compilation is costly, it would make sense to compile once and reuse the code for multiple executions.
  • The plugin code would need to have its own Wasm memory and Wasm instance.

With all these pieces together, we will get close to a canister. It might be more efficient to take canisters and adjust them for this use case rather than to implement something that is similar to canisters from scratch.

2 Likes

@ulan any thoughts on this?

1 Like
  • Components will run in the same process as the main canister, so security will be lower compared to having a separate canister.
  • Adding a new plugin component will require reinstalling the main canister (IIUC)
4 Likes