Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Guest Heap

Giving a guest a heap, so it can allocate.

cargo run --example guest_heap
use rvtime::{Config, Engine, Linker, Module, Store};

const GUEST: &[u8] = include_bytes!("../../../fixtures/hosted.elf");

fn main() -> anyhow::Result<()> {
    let mut config = Config::default();
    config.memory_size(16 << 20).stack_size(64 << 10);

    let engine = Engine::new(&config)?;
    let module = Module::new(&engine, GUEST)?;
    let mut store = Store::new(&engine, ());
    let instance = Linker::new(&engine).instantiate(&mut store, &module)?;

    // rvtime commits a heap between the guest's image and its stack, but does
    // not tell the guest where it is: the bounds travel through whatever
    // interface you defined. Here the guest exports a function to receive them.
    let heap = store.heap()?;
    println!(
        "heap: {:#x}..{:#x} ({} KiB)",
        heap.start,
        heap.end,
        (heap.end - heap.start) / 1024
    );

    let init = instance.get_typed_func::<(u64, u64), u64>("init_heap")?;
    init.call(&mut store, (heap.start, heap.end - heap.start))?;

    // With the allocator fed, the guest can use `alloc` -- `Vec`, `String`,
    // and any crate that does not need `std`.
    let sum = instance.get_typed_func::<(u64,), u64>("alloc_sum")?;
    println!(
        "guest summed a heap-allocated vector: {}",
        sum.call(&mut store, (1000,))?
    );

    // The allocator hands memory back, so a long-running guest does not leak.
    let used = instance.get_typed_func::<(), u64>("heap_used")?;
    println!("bytes still allocated: {}", used.call(&mut store, ())?);

    Ok(())
}

Why the host has to hand it over

rvtime commits the heap but does not tell the guest where it is. That looks like an omission and is deliberate.

Every way of conveying the bounds automatically means rvtime inventing an ABI: a header at a fixed guest address, a reserved call number, a value stashed in tp. Each of those claims part of a space the embedder owns, and each becomes a compatibility surface the moment anyone depends on it. So Store::heap() reports the bounds and you pass them in however your interface already works — an init export, as here, or a host function you registered.

What you get

The region sits between the guest’s image and its stack, committed read-write and zeroed. Committing it up front costs address space rather than memory: mprotect only changes protection, and pages fault in when first touched.

A guard page separates it from the stack, so running off the top of the heap faults instead of quietly landing in stack frames.

Size follows Config::memory_size — the heap is whatever is left after the image and the stack. For many small guests, size them down; the default 64 MiB is address space per store.

Before the handover

An allocation before heap::init returns null, and the guest writes through it. Guest address 0 is never committed, so that faults rather than corrupting the bottom of the address space. A null dereference in a guest is a trap you can catch, not silent damage.