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.
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 -rThe 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
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
- Clone Keystone
- Increase the available memory in
mkutils/plat/generic/run.mk(e.g., to 16G) - Increase the CMA size by setting
CONFIG_CMA_SIZE_MBYTES=1536inoverlays/keystone/configs/linux64-defconfig - Increase the filesystem size by setting
BR2_TARGET_ROOTFS_EXT2_SIZE="4G"inoverlays/keystone/configs/riscv64_generic_defconfig - Build and run Keystone according to their docs
- Once started, copy all the files via scp. You nee to use the
-Oflag, as the buildroot image does not supportsftp. The port is 9821 on localhost. The login isrootwith passwordsifive.
- The
wasm-eapp - The
wasm-happ - The
eyrie-rt, you can find it by runningfind . -name "eyrie-rt"in the keystone folder. - The
loaderyou can find it by running findfind . -name "loader" -type fin the keystone folder.
- Load the kernel module in the VM:
modprobe keystone-driver. - Execute the eapp:
./wasm-happ -l ./loader -e ./eyrie-rt -a ./wasm-eapp - 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
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.
- 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
Listenertrait for your struct, this is the entry point for edge calls. The first byte identifies the overloaded function. - Each concrete function should then... (
<MyFunction>Requestand<MyFunction>Responsewill 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>Responsetype 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 thefdis not opened for writing), this should be communicated through an error field in the<MyFunction>Responsestruct.
- Parse the request by calling
- Register the ocall to the enclave, giving it a unique CID which we will define in the next step.
- Add a CID to the
CIDenum inocall.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>Requestthe 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 anidfield, it just needs to derive the same traits.
bytemuckdoes not support enums. I would therefore define all enums as#[repr(u8)]and derivenum_enum::TryFromPrimitive.- In the response or request struct, add a private
u8field, with getters and setters which automatically cast the enum into theu8. - (The same is done with
ocall::Boolean, which alows us to have bools in aPod)
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>. bytemuckdoes not implementPodandZeroablefor arbitrarily sized arrays, so implement the unsafe traits for your custom buffer.- Implement the
DerefandDerefMuttraits 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.
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.
This project is under the Apache 2.0 license. All dependencies and submodules are under their own licenses.