Hello! I am trying to take advantage of multi-memory in combination with page size 1 in order to give untrusted wasm modules zerocopy access to Bevy engine's ECS data (stored as densely packed arrays). I provide modules access to these via Config::with_host_memory
.
In my simple test, where the allocated type has size & alignment 1, if I use a load/store to access this value stored in memory a trap is triggered (also on release which is really neat). If I use the correct load8_u/store8 instructions everything works as I'd expect!
The docs for the trait LinearMemory
state "To prevent possible silent overflows, the memory should be protected by a guard page. Additionally the safety concerns explained in 'Memory', for accessing the memory apply here as well."
Is the note about the guard page not a concern when setting Config::memory_guard_size
to 0? I'm not sure when a silent overflow might in my case.
use bevy_ecs::prelude::*;
use wasmtime::*;
#[derive(Clone)]
struct CustomMemory {
/// This pointer is not owned by CustomMemory!
ptr: usize,
size: usize,
}
unsafe impl LinearMemory for CustomMemory {
fn byte_size(&self) -> usize {
self.size
}
fn byte_capacity(&self) -> usize {
self.size
}
fn grow_to(&mut self, _new_size: usize) -> wasmtime::Result<()> {
Err(anyhow::anyhow!("It is not possible to grow this memory"))
}
fn as_ptr(&self) -> *mut u8 {
self.ptr as *mut u8
}
}
struct CustomMemoryCreator(CustomMemory);
unsafe impl MemoryCreator for CustomMemoryCreator {
fn new_memory(
&self,
ty: MemoryType,
_minimum: usize,
_maximum: Option<usize>,
reserved_size: Option<usize>,
guard_size: usize,
) -> wasmtime::Result<Box<dyn LinearMemory>, String> {
assert_eq!(guard_size, 0);
assert_eq!(reserved_size, Some(0));
assert!(!ty.is_64());
assert_eq!(ty.page_size(), 1);
Ok(Box::new(self.0.clone()))
}
}
// Note: this type is technically aligned = 1
#[derive(Resource)]
struct Res(u8);
fn main() {
let mut world = World::new();
world.insert_resource(Res(12));
{
let component_id = world.components().resource_id::<Res>().unwrap();
let ptr = world
.storages()
.resources
.get(component_id)
.unwrap()
.get_data()
.unwrap()
.as_ptr();
// Note: Res is not aligned=4, so isn't guaranteed to be aligned correctly for wasm memory
// yet this always passes because my 64 bit machine aligns allocations to 16
assert_eq!(ptr as usize % 4, 0);
// Repr should be transparent
assert_eq!(unsafe { *ptr.cast::<u8>() }, 12);
let mem_creator = std::sync::Arc::new(CustomMemoryCreator(CustomMemory {
ptr: ptr as _,
size: size_of::<Res>(),
}));
let engine = Engine::new(
Config::new()
.wasm_custom_page_sizes(true)
.with_host_memory(mem_creator.clone())
.memory_reservation(0)
.memory_guard_size(0),
)
.unwrap();
let bytes = br#"
(module
(memory (export "memory") 1 (pagesize 1))
(func (export "run")
;; Attempting a regular i32.store and i32.load would trap
(i32.store8
(i32.const 0)
(i32.add
(i32.load8_u (i32.const 0))
(i32.const 123)
)
)
)
)
"#;
let module = Module::new(&engine, bytes).unwrap();
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[]).unwrap();
let run = instance
.get_typed_func::<(), ()>(&mut store, "run")
.unwrap();
run.call(&mut store, ()).unwrap();
}
assert_eq!(world.get_resource::<Res>().unwrap().0, 123 + 12);
}
In order to respect the safety rules of wasmtime Memory and Bevy's UnsafeWorldCell I plan on scheduling systems such that no conflicts happen (a lot of the bevy ecs has most of this logic figured out already).
Is the note about the guard page not a concern when setting
Config::memory_guard_size
to 0? I'm not sure when a silent overflow might in my case.
That's correct yeah, the docs are a bit outdated there. We'll probably still recommend guard pages for safety but if there are no bugs in Wasmtime/Cranelift then there is no need for guard pages when you've configured memory_guard_size(0)
Note though that you'll want to be sure to check the minimum
variable when allocating a linear memory to ensure that it's less than self.size
bytes. That'll help prevent situations where the wasm module allocates more memory than you expect. You'll also need to handle things like memory zeroing/etc.
I'll keep a note about finding a way to add guard pages and revisit this in the future, post mvp.
be sure to check the
minimum
variable when allocating a linear memory to ensure that it's less thanself.size
bytes
Thanks for spotting that! I'll add an assertion. But it's less than or equal to right? Since resource size doesn't change I was going to adjust the min/max memory size precisely as needed.
You'll also need to handle things like memory zeroing
Ahh, good to know! I'd probably have missed this :sweat_smile: thus I did a bit of digging and I'll use runtime/vm/memory/malloc.rs
as reference for managing main module memory, since that was my next step.
Thanks a bunch for the advice Alex; and for all the amazing work you've done for the rust ecosystem!
But it's less than or equal to right?
Oh right yes, correct.
I did a bit of digging and I'll use
runtime/vm/memory/malloc.rs
as reference for managing main module memory
Oh that's a good idea, I'd definitely recommend following the rough structure there. If you come across anything we should document more clearly in LinearMemory
though please feel free to mention here or file an issue as well (or even a PR if you're up for it)
Definitely!
Last updated: Apr 07 2025 at 00:13 UTC