Stream: general

Topic: Memory Grow and Address Validation in WASM


view this post on Zulip Coulson Liang (Jun 17 2024 at 14:33):

Hi, currently we saw that the heap of the WASM module is grown by compiler emitted function __builtin_wasm_memory_grow(), and I'm curious what happens after the module calls this. How does the runtime handle this request and allocate more memory?

Also, I want to know where and how the address checking happens for WASM module. When the module try to access an address above the linear memory's current size, how is this detected and gets errored?

view this post on Zulip Lann Martin (Jun 17 2024 at 14:48):

Are you asking about Wasmtime or wasm runtimes in general?

view this post on Zulip Lann Martin (Jun 17 2024 at 14:51):

In general: __builtin_wasm_memory_grow is referring to the wasm core memory.grow instruction

view this post on Zulip Joel Dice (Jun 17 2024 at 14:51):

Regarding memory management in Wasmtime, this may be helpful: https://docs.wasmtime.dev/contributing-architecture.html#linear-memory

view this post on Zulip Coulson Liang (Jun 17 2024 at 14:57):

@Lann Martin I'm curious about how does the compiler/runtime handle this instruction

view this post on Zulip Coulson Liang (Jun 17 2024 at 15:01):

@Joel Dice Thank you this is helpful! I'm also curious what's stopping WASM to have memory shrink.
I'm thinking if it's possible implement posix style heap semantics like brk and sbrk as host functions exposed to the module, if I modify wasmtime's implementation of linear memory.

view this post on Zulip Joel Dice (Jun 17 2024 at 15:06):

A memory.shrink (or memory.discard) instruction has been discussed (e.g. https://github.com/WebAssembly/design/issues/1397), I don't know if anyone's actively working on it, though.

Hi all, after a video call with google last week, I was encouraged to raise a conversation here around issues we at Unity have with Wasm memory allocation. The short summary is that currently Wasm ...

view this post on Zulip Lann Martin (Jun 17 2024 at 15:12):

I think part of the reason we haven't seen a memory.shrink is that for many systems using Wasm it is acceptable - or even desirable - to have short-lived instances recreated for each unit of work (e.g. request) rather than having long-running instances that need memory reclamation.

view this post on Zulip Coulson Liang (Jun 17 2024 at 17:56):

So I saw there are 4 types of RuntimeLinearMemory,
MmapMemory StaticMemory MmapMemoryProxy SharedMemory, usually is a wasm module attached to one or more than one of these RuntimeLinearMemory instances?
I saw in wasi-libc, when memory.grow is called, the first argument (memory index) is always 0, so I suppose the 0 th memory instance is always the main MmapMemory

Also, I might need some help to find the heap bound checking code in wasmtime :joy:

view this post on Zulip Alex Crichton (Jun 17 2024 at 18:13):

The specifics there are MmapMemory is what most modules use as it's the default, StaticMemory is used with the pooling allocator, MmapMemoryProxy IIRC is used for the embedder API as an opt-in for host-defined memories, and SharedMemory is used for shared memories (e.g. memory for threads). Each linear memory is only one of these, and most modules have a single linear memory. If a module has multiple linear memories each one could be backed by a unique one though.

I suppose the 0 th memory instance is always the main MmapMemory

More-or-less: yes. The 0 here is "memory 0" where 0 is the index of memory in the module memory index space. The Rust/C/C++ memory model doesn't support more than one memory most of the time, so typically there's only a single memory at index 0. And yes it's most of the time MmapMemory as that's the default.

Also, I might need some help to find the heap bound checking code in wasmtime

This is intertwined in a few places. Throughout the host API bounds-checks happen as the Rust view of linear memory is &[u8] which is bounds-checked by default. In compiled code however there's a few different ways that bounds are represented and it could be compiled a number of different ways.

Put another way there's not a single place for bounds checks in wasmtime, it's spread all over as-needed. Do you have a particular bounds-check in mind or an area you were thinking of focusing on?

view this post on Zulip Coulson Liang (Jun 17 2024 at 18:46):

Oh yeah thank you for the explanations! I'm really interested in the case, say the c program just call malloc or sbrk to grow the linear memory to X, then the program try to access an address higher than X, how is this error caught.?

view this post on Zulip Alex Crichton (Jun 17 2024 at 18:49):

Those sorts of errors are caught via segfaults and signal handling. If wasm accesses memory outside of its bounds then that's, by default, guaranteed to be unmapped memory. Cranelift translation for bounds checks happens in this file, but I'll note that this is a tricky part of Wasmtime since we implement a few strategies for bounds checks in compiled code.

By default though there are no bounds checks. We reserve 8G of virtual memory as "unmapped" to start out for all linear memories. Starting 2G into this region is where the linear memory itself resides, often starting around a size of ~1M. Growth happens by mapping pages in. Out-of-bounds is done by the wasm actually does the load/store and it ends up being unmapped memory, triggering a segfault, and then the segfault is translated to an out-of-bounds error

A fast and secure runtime for WebAssembly. Contribute to bytecodealliance/wasmtime development by creating an account on GitHub.

view this post on Zulip Coulson Liang (Jun 17 2024 at 19:06):

Thanks, I see how this works. I think I don't need to change this bound_ckecking part of cranelift, I just want to know what are the LinearMemory metadata the bound checker looks for to know which region is valid.
As you said, I guess the bound checker know this info by the metadata of the RuntimeLinearMemory instance. So when memory.grow is called, the MmapMemory increase its size, and then the bound checker know the update bound. I'm digging deeper to see if this is true.

view this post on Zulip Alex Crichton (Jun 17 2024 at 19:08):

That's basically correct yeah, although one thing I can clarify is that we're trying to keep two pieces of data in sync, what's mapped and what the length of the linear memory is. The "source of truth" depends on who's asking. For example if compiled code is asking then the source of truth is what's mapped and what isn't. For embedder/host things the source of truth is the length field. The job of RuntimeLinearMemory is basically to keep these two in sync

view this post on Zulip Coulson Liang (Jun 17 2024 at 19:20):

This makes perfect sense, thank you!

view this post on Zulip Ralph (Jun 18 2024 at 10:51):

https://bytecodealliance.zulipchat.com/#narrow/stream/206238-general/topic/Memory.20Grow.20and.20Address.20Validation.20in.20WASM/near/445156870 this! for example, for a typical serverless function even GC just slows you down, as you'll recycle the memory at the end of invocation. The things that like to shrink memory are longer running functions -- once you get to changing wasm spec to support an older runtime style you should be thinking if there's a way to lean into the feature, not change it.

view this post on Zulip Ralph (Jun 18 2024 at 10:53):

doesn't mean you can't! it's possible to shrink, of course. However, if you think about other features of wasm, such as the lack of readonly memory inside the module, you suddenly realize that an "OS-like" long running module is more problematic than running a container. You still might do it!!! When you really need the portability above all, for example.

view this post on Zulip Ralph (Jun 18 2024 at 10:53):

ymmv


Last updated: Dec 23 2024 at 12:05 UTC