Skip to content

ThalesGroup/WasmStone

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WasmStone

This repository contains the WasmStone project, as described in the paper WasmStone: enabling transparent execution of standalone WebAssembly applications in Keystone presented at ARES 2026.

Building

Building this project

Enclave and Host must be built independently, as the dependencies do not like each other and a workspace would build the union of all features.

# Inside happ
cargo b --target riscv64gc-unknown-linux-gnu -r

# Inside eapp
cargo b --target riscv64gc-unknown-none-elf -r

Building Keystone for QEMU

The instructions for building and running Keystone for QEMU can be found here: https://docs.keystone-enclave.org/en/latest/Getting-Started/Running-Keystone-with-QEMU.html

Building Keystone for the VisionFive 2

The sdcard image for Keystone on the VisionFive 2 can be built using this Dockerfile: https://github.com/vector-sdk/vector-keystone/blob/main/docker/Dockerfile.vf2

Running on Keystone

  1. Clone Keystone
  2. Increase the available memory in mkutils/plat/generic/run.mk (e.g., to 16G)
  3. Increase the CMA size by setting CONFIG_CMA_SIZE_MBYTES=1536 in overlays/keystone/configs/linux64-defconfig
  4. Increase the filesystem size by setting BR2_TARGET_ROOTFS_EXT2_SIZE="4G" in overlays/keystone/configs/riscv64_generic_defconfig
  5. Build and run Keystone according to their docs
  6. Once started, copy all the files via scp. You nee to use the -O flag, as the buildroot image does not support sftp. The port is 9821 on localhost. The login is root with password sifive.
  • The wasm-eapp
  • The wasm-happ
  • The eyrie-rt, you can find it by running find . -name "eyrie-rt" in the keystone folder.
  • The loader you can find it by running find find . -name "loader" -type f in the keystone folder.
  1. Load the kernel module in the VM: modprobe keystone-driver.
  2. Execute the eapp: ./wasm-happ -l ./loader -e ./eyrie-rt -a ./wasm-eapp
  3. Inside the enclave, run the classifier which is compiled to pulley. The path must be relative to where you started the enclave: run classifier.cwasm

Adding new Edge Calls

The edge call space of Keystone is limited to 10. This means that we have to overload several edge calls to implement the entire WASIp2 API. Additionaly, non-wasi edge calls have proven themeselves to be very useful, such as all logging calls. The log_<level> methods allow us to follow the internals of the runtime. I generally tried to introduce one edge call per world (e.g., one for sockets, one for io, ...), however, I also merged smaller worlds into a "general" edge call (e.g. random and cli).

The following example will explain how to add an entirely new edge call. Having understood this example will hopefully make most of the code clear and will also allow you to add new overloads to existing edge calls. For more details, look at concrete implementations of e.g., io.

In the Host App

  • Add a new struct for your edge call in lib.rs, e.g., NetHandler
  • If the struct should have access to global state, add a state: Arc<Mutex<State>> field
  • Implement the Listener trait for your struct, this is the entry point for edge calls. The first byte identifies the overloaded function.
  • Each concrete function should then... (<MyFunction>Request and <MyFunction>Response will be defined in the next step)
    • Parse the request by calling let rr: <MyFunction>Request = *bytemuck::from_bytes(ctx.request())
    • Handle the function based on the data in <MyFunction>
    • Create a let response: <MyFunction>Response type with the result
    • Call let data = bytemuck::bytes_of(&response)
    • Write the data into ctx.response
    • The function should nearly always return Success, except if something related to Keystone fails. For function-related errors, (e.g., a write fails because the fd is not opened for writing), this should be communicated through an error field in the <MyFunction>Response struct.
  • Register the ocall to the enclave, giving it a unique CID which we will define in the next step.

In the Shared Library

  • Add a CID to the CID enum in ocall.rs
  • Add a new module for your edge call under ocall/<new>.rs
  • Define an enum for the different overloads:
#[derive(Debug, PartialEq, Eq, Clone, Copy, TryFromPrimitive)]
#[repr(u8)]
pub enum <New>Cmd {
    <MyFunction>
}
  • Define <MyFunction>Request the following way:
#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
#[repr(C)]
pub struct <MyFunction>Request {
    /// This should be hidden and always initialized in the "constructor".
    /// It is used in the host to identify the overload. It is therefore
    /// critical that this is always the first field in the struct.
    id: u8,
    // ... more fields
}

impl <MyFunction>Request {
    // Also initialize fields, if needed
    pub fn new() -> Self {
        Self {
            id: <New>Cmd::<MyFunction> as u8
        }
    }
}
  • Define <MyFunction>Response, which no longer needs an id field, it just needs to derive the same traits.

Tipps

Handling Enums
  • bytemuck does not support enums. I would therefore define all enums as #[repr(u8)] and derive num_enum::TryFromPrimitive.
  • In the response or request struct, add a private u8 field, with getters and setters which automatically cast the enum into the u8.
  • (The same is done with ocall::Boolean, which alows us to have bools in a Pod)
Handling Dynamicically Sized Data

Generally the shard::ocall::io::{ReadResponse, WriteBlockingRequest} are good examples for types with unknown size at compile time. It generally works like this:

  • Define a Buffer type with a length of MAX_INPUT_LENGTH - <the size for the rest of the struct>.
  • bytemuck does not implement Pod and Zeroable for arbitrarily sized arrays, so implement the unsafe traits for your custom buffer.
  • Implement the Deref and DerefMut traits for the buffer
  • Hide direct access to the entire buffer in the parent request/response struct, and only allow access through a function, which returns a slice based on the indicated length. This is very useful, as it allows us to automatically read the correct amount of data, and not parts of the array which are not actually part of the message. To wrap up, dynamically sized edge calls will always make use of the full buffer, but might only write to parts of the buffer. It makes sense to follow the given implementations as closely as possible.

In the Enclave App

Very easy: just create a new <MyFunction>Request and call ocall_bytemuck, which will then return a <MyFunction>Response (compiler has to know the response type at compile time though). ocall_bytemuck should only fail if there is an issue related to Keystone, issues with the call itself should be passed in the <MyFunction>Response.

License

This project is under the Apache 2.0 license. All dependencies and submodules are under their own licenses.

About

WasmStone: enabling transparent execution of standalone WebAssembly applications in Keystone

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors