user23844397
user23844397

Reputation: 21

How to pass a byte array to a WASM module from wasmer in Rust?

I have a WASM module as follows

#[no_mangle]
pub extern "C" fn process_bytes(ptr: *const u8, len: usize) -> u8 {
    if len > 0 {
        let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
        let first = bytes[0];

        first
    } else {
        0
    }
}

It accepts a byte array, returning the first byte.

This module is compiled with cargo build --target wasm32-unknown-unknown --release then called from another crate that uses wasmer

pub mod get_tx_data;

use wasmer::{imports, Cranelift, Instance, Module, Store, Value};

fn main() {
    // Read the Wasm file
    let wasm_bytes = include_bytes!("banana_swap.wasm");

    // Create a store
    let mut store = Store::default();

    // let compiler = Cranelift::default();
    // let mut store = Store::new(compiler);

    // Compile the module
    let module = Module::new(&store, wasm_bytes).unwrap();

    // Create an import object with the host function
    let import_object = imports! {};

    let instance = Instance::new(&mut store, &module, &import_object).unwrap();
    let function = instance.exports.get_function("process_bytes").unwrap();

    // Example byte array
    let byte_array: Vec<u8> = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F];

    // Call the exported function with the byte array
    let result = function.call(&mut store, &[
        Value::I32(byte_array.as_ptr() as i32),
        Value::I32(byte_array.len() as i32)
    ]).unwrap();

    // Check the result
    println!("Result: {:?}", result);

}

This gives error signal_trap: Some(HeapAccessOutOfBounds) }. The array length is being read properly, I'm able to return it from the function. What went wrong?

thread 'main' panicked at indexer-core/src/main.rs:31:8:
called `Result::unwrap()` on an `Err` value: RuntimeError { source: Wasm { pc: 127833057173522, backtrace:    0: <unknown>
   1: <unknown>
   2: <unknown>
   3: <unknown>
   4: <unknown>
   5: <unknown>
   6: <unknown>
, signal_trap: Some(HeapAccessOutOfBounds) }, wasm_trace: [FrameInfo { module_name: "<module>", func_index: 0, function_name: Some("process_bytes"), func_start: SourceLoc(119), instr: SourceLoc(137) }] }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Source code for reproduction

https://github.com/icedancer-io/indexer-core/tree/stackoverflow

Use the stackoverflow branch. How to run-

cd banana-swap && cargo build --target wasm32-unknown-unknown --release && cd .. && cp ./target/wasm32-unknown-unknown/release/banana_swap.wasm indexer-core/src/banana_swap.wasm && cargo run --bin indexer-core

Upvotes: 1

Views: 421

Answers (2)

Caesar
Caesar

Reputation: 8504

You passed a pointer to host memory to process_bytes. The WASM module instance gets its entirely separate block of memory, so when it tries to access that pointer, it's a pointer to nowhere. You'll have to copy the byte array to the instance's memory.

Sorry, this is only a sketch of how to do that:

  1. You'll somehow have to obtain a valid instance memory pointer on the host. One way of doing that is to export malloc (and call it):
    #[no_mangle]
    pub extern "C" fn alloc(len: u32) -> u32 {
      let layout = std::alloc::Layout::array::<u8>(len.try_into().unwrap()).unwrap();
      unsafe {
        (std::alloc::alloc(layout) as usize).try_into().unwrap()
      }
    }
    
  2. Write the data to instance memory:
    memory.view(&store).write(pointer_from_alloc, &byte_array);
    
  3. Call your function:
    function.call(&mut store, &[Value::I32(pointer_from_allc), Value::I32(byte_array.len() as i32)])
    
  4. Dealloc ;)

Of course, there's other ways of getting valid writable pointers, for example Memory::grow, call a host import from wasm with a pointer (that'd be what wasi's fd_write does), which can then call process_bytes, or just reserving a static array at a specific location.

Upvotes: 0

Chayim Friedman
Chayim Friedman

Reputation: 70990

WASM cannot access regions outside of its allocated memory (of the host). That would defeat the entire point of sandboxing using WASM.

You need to copy the data into WASM memory, then pass the address from there.

use wasmer::{imports, Instance, Module, Store, Value};

fn main() {
    // Read the Wasm file
    let wasm_bytes = include_bytes!("../../target/wasm32-unknown-unknown/release/guest.wasm");

    // Create a store
    let mut store = Store::default();

    // let compiler = Cranelift::default();
    // let mut store = Store::new(compiler);

    // Compile the module
    let module = Module::new(&store, wasm_bytes).unwrap();

    // Create an import object with the host function
    let import_object = imports! {};

    let instance = Instance::new(&mut store, &module, &import_object).unwrap();
    let function = instance.exports.get_function("process_bytes").unwrap();

    // Example byte array
    let byte_array: Vec<u8> = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F];

    let memory = instance.exports.get_memory("memory").unwrap();
    let view = memory.view(&store);
    // Cannot be 0 because null is disallowed in Rust references.
    view.write(1, &byte_array).unwrap();

    // Call the exported function with the byte array
    let result = function
        .call(
            &mut store,
            &[Value::I32(1), Value::I32(byte_array.len() as i32)],
        )
        .unwrap();

    // Check the result
    println!("Result: {:?}", result);
}

If you don't know a free place in the memory, you can for example ask the guest to allocate X bytes using its allocator and return their address, then write there.

Upvotes: 1

Related Questions