Skip to content

Function::from_vm_funcref unconditionally grows the Store's function arena, leaking memory when the same funcref is resolved repeatedly #6851

Description

@Arshia001

Summary

Function::from_vm_funcref (sys backend) unconditionally allocates a new StoreHandle and pushes it into the Store's function arena every time it's called, with no caching or deduplication. Since this function is on the hot path for converting any funcref-typed value crossing the host/guest boundary — not just one-time instantiation-time wrapping — a host embedding that repeatedly resolves the same underlying guest function (e.g. a callback passed as an argument, invoked many times) leaks one arena entry per resolution. In a long-running process this grows unboundedly and is never reclaimed.

Root cause

lib/api/src/backend/sys/entities/function/mod.rs, Function::from_vm_funcref:

pub(crate) unsafe fn from_vm_funcref(store: &mut impl AsStoreMut, funcref: VMFuncRef) -> Self {
    let signature = {
        let anyfunc = unsafe { funcref.0.as_ref() };
        store.as_store_mut().engine().as_sys()
            .lookup_signature(anyfunc.type_signature_hash)
            .expect("Signature not found in store")
    };
    let vm_function = VMFunction {
        anyfunc: MaybeInstanceOwned::Instance(funcref.0),
        signature,
        kind: wasmer_vm::VMFunctionKind::Static,
        host_data: Box::new(()),
    };
    Self { handle: StoreHandle::new(store.objects_mut().as_sys_mut(), vm_function) }
}

StoreHandle::newInternalStoreHandle::new (lib/vm/src/store.rs) always does list.push(val) into StoreObjects.functions: Vec<VMFunction>:

pub fn new(ctx: &mut StoreObjects, val: T) -> Self {
    let list = T::list_mut(ctx);
    let idx = NonZeroUsize::new(list.len() + 1).unwrap();
    list.push(val);          // unconditional append, every single call
    Self { idx, marker: PhantomData }
}

StoreObjects has no removal/GC method for any of its nine object vectors (functions, tables, globals, instances, memories, extern_objs, exceptions, tags, function_environments) — reasonable for objects that are normally created once at module instantiation, but from_vm_funcref is not limited to that use case.

from_vm_funcref is reachable from every dynamic funcref crossing, not just instantiation:

  • Value::from_raw's Type::FuncRef arm (lib/api/src/entities/value.rs), hit whenever a funcref-typed value (parameter or return value) is marshalled from a raw ABI value.
  • NativeWasmTypeInto::from_abi/from_raw for Option<Function> (lib/api/src/utils/native/convert.rs), on the typed-function/native-ABI call path.
  • Table::get (lib/api/src/backend/sys/entities/table.rs) on any funcref-typed table element, via value_from_table_element.

Contrast with the sibling from_vm_extern (same file), used for the same purpose by Table/Global/Memory/Tag:

pub(crate) fn from_vm_extern(store: &mut impl AsStoreMut, vm_extern: VMExternFunction) -> Self {
    Self { handle: unsafe { StoreHandle::from_internal(store.objects_mut().id(), vm_extern.unwrap_sys()) } }
}

This one just wraps an existing handle/index — zero allocation, no Vec::push. Function has no equivalent path because a VMFuncRef crosses the boundary as a bare pointer rather than an existing store index, so from_vm_funcref always fabricates a new entry instead of being able to look one up.

The underlying VMFuncRef's pointer (NonNull<VMCallerCheckedAnyfunc>) is stable per instance-local-function / per host Function for the life of the instance (traced through Instance::funcrefs / build_funcrefs in lib/vm/src/instance/mod.rs), and VMFuncRef already derives Hash/Eq/PartialEq by that pointer's address (lib/vm/src/lib.rs):

#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct VMFuncRef(pub NonNull<VMCallerCheckedAnyfunc>);

So the same funcref reliably produces the same pointer value across repeated resolutions — nothing currently deduplicates against it.

Impact

Any embedding that repeatedly converts the same funcref-typed value into a wasmer::Function — e.g. a host holding a reference to a guest callback function and invoking it many times via Table::get + Function::call, or any code path that re-derives a Value::FuncRef from raw ABI values on each call — leaks one VMFunction entry (plus a cloned FunctionType from SignatureRegistry::lookup_signature, itself heap-allocated) per resolution, for the lifetime of the Store. In a long-running host process handling many such calls, this is unbounded memory growth with no way to reclaim it short of dropping the entire Store.

Suggested fix

Add a dedup cache to StoreObjects (or scoped to Function::from_vm_funcref itself) keyed by the VMFuncRef's pointer identity — e.g. HashMap<NonNull<VMCallerCheckedAnyfunc>, InternalStoreHandle<VMFunction>> (or keyed on VMFuncRef directly, since it already has the needed Hash/Eq impls) — checked before constructing a new VMFunction/pushing, mirroring how from_vm_extern already reuses an existing handle instead of allocating. No lock should be needed since StoreObjects is always accessed via &mut. This would also fix Table::get and the typed-call/native-ABI funcref conversions for free, since they all funnel through the same entry point. Worth checking whether the v8/js backends' own from_vm_funcref implementations have the same issue.

Reproduction sketch

  • Export/import a WASM function that returns (or receives) a funcref value pointing at the same underlying guest function on every call.
  • On the host side, resolve that funcref to a wasmer::Function (via Value::from_raw, a typed call with a FuncRef/Option<Function> argument or return type, or Table::get) repeatedly, e.g. in a loop or once per incoming request in a server-like workload.
  • Observe StoreObjects.functions (or overall process RSS while the Store stays alive) grow linearly with the number of resolutions rather than staying bounded by the number of distinct funcrefs actually in use.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions