How to implement realloc of C in Wasmtime?

Hi, I'm trying to implement realloc in Wasmtime.

// source.c
#include <stdio.h>
#include <stdlib.h>
// I hope this can do the same thing as realloc
extern my_realloc(void *ptr, size_t size);

int main() {
  int *ptr = malloc(sizeof(int)*3);
  my_realloc(ptr, 6);
  return 0;
}

Then I'll use clang, llc, wasm-ld to compile source.c to source.wasm. After that, I want to run it via wasmtime.

And in Wasmtime, I'll use Linker to wrap the function to implement it.

// src/commands/run.rs
//...
linker.func_wrap("env", "my_realloc", |mut caller: Caller<'_, Host>, ptr: u32, size: u32|{
  // what should I do here?
})?;
//...

But the problem here is that I have no idea how to manipulate the wasm memory to make it meet my needs.

And I know the orial realloc function will call dlmalloc, but still I got no idea how to make this work.

From the point of view of wasmtime the wasm memory is just a bunch of bytes. What bytes are part of an allocation is determined by the allocator, which in your case is whatever the C code used, and is embedded in the C program. So the only way to do what you want is to either reimplement the logic used by the C code or to call an exposed function from the C module that does what you want (although why would you use an external function at that point?)

1 Like

The host shouldn't be (and can't!) implement realloc.

The guest (your WebAssembly code) has its own allocator implementation built into it, and that allocator is the one which will provide a realloc() implementation. This allocator should also be smart enough to call the core::arch::wasm32::memory_grow() instruction when it wants Wasmtime to give it more memory.

If the guest is Rust, you can create an implementation of the std::alloc::GlobalAlloc trait and register it as the global allocator by adding the #[global_allocator] attribute to some static instance of your allocator.

Normally, if the host needs to make allocations in guest memory (e.g. it wants to allocate a buffer and write data into it), it's up to the guest to export malloc() and free() functions that the host can invoke.

3 Likes

That makes sense. Thanks a lot. :smile:

1 Like

Hi, I understand that the host shouldn't implement realloc or other memory relative functions, but I still need some way to make it work.

Here is a C file, I'll still compile it into test.wasm and run it via wasmtime test.wasm.

// test.c
#include <stdio.h>
#include <stdlib.h>

extern void test_func(int *ptr, int size);

int main() {
    int *p = malloc(sizeof(int) * 6);
    test_func(p, 9);
    return 0;
}

And in wasmtime, I wrap the function test_func like below and register it in Linker. And actually wasmtime will call test_func which is declared in helper.c which is shown down here.

// wasmtime/src/commands/run.rs
linker.func_wrap("env", "test_func", wrap_test_func)?;
// ...
#[link(name = "my-helpers")]
extern "C" {
    fn test_func(ptr: *const u32, size: i32);
}

fn wrap_test_func(mut caller: wasmtime::Caller<'_, Host>, _ptr: u32, size: i32) {
    let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
    let linear_memory: &[u8] = memory.data(&caller);
    
    unsafe {
        let ptr: *const u32 = linear_memory.as_ptr().add(_ptr as usize).cast();
        test_func(ptr, size)
    }
}

helper.c will be compiled to my-helpers via build.rs which I didn't show it here.

// helper.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void test_func(int *ptr, int size) {
    int *new_ptr;
    new_ptr = realloc(ptr, size);
    ptr = new_ptr;
    printf("%d\n", ptr[1]);
}

The most trickey thing happens here is that I will call realloc in helper.c.

So when I actually run wasmtime test.wasm, the error will occur. The error message is here.

wasmtime(90045,0x104bb8580) malloc: *** error for object 0x300010610: pointer being realloc'd was not allocated
wasmtime(90045,0x104bb8580) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    90045 abort      ./wasmtime test.wasm

The main problem here is still that I want to manipulate the wasm memory in host side, and in my code, it's obviously improper. So I wonder if there is a way to fix this.

This doesn't look right to me.

Your test_func() is running on the host and ptr points to an allocation made by the guest's allocator inside its linear memory.

However, the realloc() function in helper.c will try to reallocate the pointer using the host's allocator. This involves potentially freeing ptr, but because your host's allocator didn't allocate that memory in the first place (the guest did), the allocator detects something has gone seriously wrong and crashes.

Your problem is that you've mixed allocators up. If you want to reallocate a buffer the guest gave you, you must use the realloc() function exported by the guest, not the one from stdlib.h.

Yeah, I understand it totally. Here is a absolutely wrong case.

The problem I got here is that how can I export the realloc or other memory relative API like malloc, free.

Because in my case, my host is Wasmtime and my guest is seemingly to be C, but actually Wasm which makes me have no idea how to export the realloc properly.

You can take a look how proxy-wasm handles this – they require the client to expose a callback proxy_on_memory_allocate:

And this is eg. how Rust proxy-wasm SDK implements this callback:

1 Like

If you need malloc and friends, you should be able to provide your own wrappers:

__attribute__((export_name("malloc")))
void *my_malloc(size_t size) { return malloc(size); }

__attribute__((export_name("free")))
void my_free(void *ptr) { free(ptr); }

__attribute__((export_name("realloc")))
void *my_realloc(void *ptr, size_t size) { return realloc(ptr, size); }

When hosts are trying to manually allocate buffers inside the guest's linear memory, this is usually the approach they'll take.

This is also why I was suggesting to use something like wit-bindgen. Managing memory and objects across the host-guest boundary is tricky and often unsafe[1], so it's much easier to use a tool that'll generate correct glue code for you. You'll also find that being forced to write purely safe Rust means bugs like mixing up the allocator just can't happen.

Another thing you might find helpful if you are only using Rust as a way to connect Wasmtime with your C code is that there are C APIs for both Wasmer (here) and Wasmtime (here). Both runtimes follow the WebAssembly C API, so they are largely interchangeable.


  1. Well, you can definitely avoid the unsafe, but it makes the code considerably more cumbersome and you don't actually gain much safety because it's perfectly safe (from Rust's perspective) for the host to stomp all over the guest's memory space, even if it would trigger UB in the guest. Linear memory is just a Vec<u8> after all. ↩ī¸Ž

1 Like

Thanks a lot! I finally make it!

This really helps me! How didn't I think of it :smile:

1 Like