posborne opened issue #12984:
Feature
A common concern for embedders is that the amount of memory that a guest may allocate in the host be limited. Today, wasmtime provides some bounds for provided implementations but it is generally a concern that lands on embedders to sort out for themselves. Very crude limits are available today in two recently added configuration options:
I propose that we provide a means for limits to be defined and enforced by wasmtime for hosts that are making use of wasmtime's ResourceTable. This mechanism would track all inserts, deletes and modifications to the ResourceTable values and prevent the aggregate size of heap allocations in the host for a given ResourceTable/guest from exceeding the configured limit.
Benefit
This change would provide blanket coverage to provide and easy-to-configure mechanism to protect against a class of DOS attacks without significant special logic required by each embedder. It is at a level of granularity that should be reasonable for embedders to reason about and may allow for embedders to increase limits like the max capacity on the ResourceTable with more confidence.
Drawbacks
Implementing this change will require changes to implement trait(s) that give information to wasmtime about the size of values to be stored in the ResourceTable. This will have two implications:
- A fairly large change to land in wasmtime with some associated risk.
- A breaking change to embedders on wasmtime upgrade to correctly implement the required traits.
In addition, this will add some overhead to operations that mutate the resource table, though in general I expect they should generally not be too onerous given that we are on a path already performing allocations or mutations. If the resource contains complex types (e.g. a collection of a heap allocated items), this cost can grow. Embedders can provide cheaper implementations for these complex types if they so choose.
Implementation
I have a proof-of-concept implementation here: https://github.com/bytecodealliance/wasmtime/compare/main...posborne:wasmtime:resource-table-host-heap-limiter?expand=1. for now, I am creating this issue first to work through high-level feedback. Some types are opaque and a complete implementation of traits is not possible (but this can be OK with how tracking works so long as the associated heap memory for opaque types is reasonable -- something already assumed today).
I propose basing tracking around two traits:
/// Trait for types that can report their host heap memory usage. /// /// This trait must be implemented by any type `T` that can be stored in a /// [`ResourceTable`]. The `host_heap_usage` method should return an estimate /// of the total memory (in bytes) allocated for this resource, including the /// inline size of the value itself (`std::mem::size_of_val(self)`) plus any /// additional heap allocations it owns (e.g. `Vec` capacity, `String` /// capacity, nested boxed values, etc.). /// /// The minimum correct implementation for any type is /// `std::mem::size_of_val(self)`, which accounts for the inline footprint that /// the resource table entry occupies. Returning `0` is never correct. /// /// # Examples /// /// ``` /// use wasmtime::component::HostHeapUsage; /// /// struct MyResource { /// name: String, /// buffer: Vec<u8>, /// } /// /// impl HostHeapUsage for MyResource { /// fn host_heap_usage(&self) -> usize { /// std::mem::size_of_val(self) /// + self.name.capacity() /// + self.buffer.capacity() /// } /// } /// ``` /// /// [`ResourceTable`]: super::ResourceTable pub trait HostHeapUsage { /// Returns the number of bytes of memory used by this value, including its /// inline size and any heap allocations it owns. fn host_heap_usage(&self) -> usize; } /// Marker trait for types whose host heap footprint is **constant** — that is, /// mutation through `&mut self` can never change the value returned by /// [`HostHeapUsage::host_heap_usage`]. /// /// This is the case for any type that owns no separately heap-allocated data: /// plain structs and enums composed of scalars, `Arc`/`Rc` wrappers (the /// pointee is shared, not solely owned), OS handles, and zero-sized types. /// /// # Safety contract /// /// Implementing this trait is a promise that `host_heap_usage()` returns the /// same value before and after any `&mut self` mutation. Violating this /// contract will cause the [`ResourceTable`]'s usage counter to silently drift. /// /// Implementing `FixedHostHeapUsage` automatically provides a [`HostHeapUsage`] /// implementation via a blanket impl that returns `core::mem::size_of_val(self)`. /// You do not need to implement `HostHeapUsage` separately. /// /// # Payoff /// /// Types that implement `FixedHostHeapUsage` may use /// [`ResourceTable::get_mut`], which returns a plain `&mut T` with no /// overhead. Types that only implement [`HostHeapUsage`] must use /// [`ResourceTable::update_resource`], which accepts a closure and updates /// the table's usage counter around the mutation. /// /// # When to implement this trait /// /// Implement `FixedHostHeapUsage` when every byte owned by the value lives /// *inline* within the value itself: primitives, plain enums, structs composed /// entirely of such types, `Arc`/`Rc` wrappers (where the pointee is shared and /// not solely owned by this value), OS handles, and zero-sized types. /// /// Do **not** implement this trait when the type owns heap allocations whose /// size grows with runtime data — for example types with `String`, `Vec<T>`, /// `Box<T>`, or `HashMap` fields. For those types, implement [`HostHeapUsage`] /// directly and use [`ResourceTable::update_resource`] for mutation. /// /// [`ResourceTable`]: super::ResourceTable pub trait FixedHostHeapUsage: Sized {} /// Blanket [`HostHeapUsage`] implementation for all [`FixedHostHeapUsage`] types. /// /// Returns `core::mem::size_of_val(self)`, which is the complete and correct /// footprint for any type whose heap usage never changes through mutation. impl<T: FixedHostHeapUsage> HostHeapUsage for T { fn host_heap_usage(&self) -> usize { core::mem::size_of_val(self) } }Table ops are then updated to require the
HostHeapUsagetrait.get_mutis updated to only support operations onFixedHostHeapUsage. Callsites that need to work more generally must use new methods likeupdate_resourcethat take a closure for performing mutations of a resource (which may change the size). An approach using a guard is also possible, but didn't work out too well as presently the limit checks are immediate and I wanted to avoid having to come up with a more complex solution to signal from Drop that the limit has been exceeded.Alternatives
I think some form of this change likely makes sense, though the details of the traits, table mutation patterns, and place at which limit enforcement should take place remain areas where I am very interested in alternative ideas.
I did consider attempting some kind of proc-macro that could help derive HostHeapUsage, but was not convinced this would be a fruitful area of exploration. Ultimately, the consequences of an incorrect trait impl are not too bad, so long as they are consistent.
alexcrichton commented on issue #12984:
Thanks for writing this up and for prototyping the change! Overall this looks pretty good to me, and personally I'd be willing to take the hit of the breaking change here as tracking this metadata seems more worthwhile than not.
I'd bikeshed the exact shape of these traits a bit in relatively minor ways, though. There's some prior art from Servo with the heapsize and malloc_size_of crates here. I don't think we will want to literally use either of those, but one major difference is how
selfis accounted for in byte sizes. It's relatively cumbersome in this PR to always be sure to add inself's size and sometimes also have somesaturating_subs to remove double-counted bytes. I think with some reworking along those crates it might be possible to avoid this? For example having the method be "heap size of my children" I think might help centralize the computations and avoid some duplication.
posborne commented on issue #12984:
It's relatively cumbersome in this PR to always be sure to add in self's size and sometimes also have some saturating_subs to remove double-counted bytes.
@alexcrichton That's great feedback and I agree -- It was at the end of this first pass that I was filling in a few impls for more complex types and started to bump into this more. I'll incorporate that feedback prior to opening the PR.
Last updated: Apr 12 2026 at 23:10 UTC