Skip to content

Commit c8c38bc

Browse files
chore: improve ResourceLimiter
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 64fdb71 commit c8c38bc

10 files changed

Lines changed: 76 additions & 68 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4848
- Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`.
4949
- Removed the pluggable memory backend system (`LinearMemory`, `MemoryBackend`, `VecMemory`, `PagedMemory`, `LazyLinearMemory`, and `Config::with_memory_backend`). Linear memory is always `Vec`-backed. To limit initial memory allocation and growth, configure a `ResourceLimiter` with `Config::with_resource_limiter`.
5050
- Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly.
51+
- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory allocation or growth request.
5152

5253
## [0.10.0] - 2026-07-24
5354

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ TinyWasm modules can be compiled to the internal `twasm` bytecode format, which
6161

6262
With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments.
6363

64-
Use `Engine` and `engine::Config` when you need non-default runtime settings such as fuel accounting, stack sizing, the GC collection threshold, or trap-on-OOM behavior. A `ResourceLimiter` attached to the engine's config bounds guest memory allocation and growth.
64+
Use `Engine` and `engine::Config` when you need non-default runtime settings such as fuel accounting, stack sizing, or the GC collection threshold. A `ResourceLimiter` attached to the engine's config bounds guest memory allocation and growth and can trap rejected requests.
6565

6666
[^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`.
6767

‎crates/cli/src/engine_flags.rs‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ pub struct EngineFlags {
88
#[arg(long, value_enum)]
99
pub fuel_policy: Option<FuelPolicyArg>,
1010

11-
/// Trap immediately on memory or stack allocation failure
12-
#[arg(long)]
13-
pub trap_on_oom: bool,
14-
1511
/// Fixed value stack size for all value lanes
1612
#[arg(long, conflicts_with = "value_stack_dynamic")]
1713
pub value_stack_size: Option<usize>,
@@ -88,10 +84,6 @@ impl EngineFlags {
8884
config = config.with_call_stack(call_stack_dynamic.into_stack_config());
8985
}
9086

91-
if self.trap_on_oom {
92-
config = config.with_trap_on_oom(true);
93-
}
94-
9587
Ok(Engine::new(config))
9688
}
9789
}

‎crates/tinywasm/src/engine.rs‎

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ impl StackConfig {
9696
/// .with_value_stack_32(StackConfig::dynamic(1024, 36 * 1024))
9797
/// .with_value_stack_64(StackConfig::dynamic(1024, 32 * 1024))
9898
/// .with_value_stack_128(StackConfig::dynamic(256, 4 * 1024))
99-
/// .with_call_stack(StackConfig::dynamic(64, 1024))
100-
/// .with_trap_on_oom(true);
99+
/// .with_call_stack(StackConfig::dynamic(64, 1024));
101100
///
102101
/// assert!(matches!(config.fuel_policy(), FuelPolicy::Weighted));
103102
/// ```
@@ -117,9 +116,6 @@ pub struct Config {
117116
pub call_stack: StackConfig,
118117
/// Fuel accounting policy used by budgeted execution. Defaults to [`FuelPolicy::PerInstruction`].
119118
pub fuel_policy: FuelPolicy,
120-
/// Whether memory and stack allocation failures should trap instead of degrading into normal operation failure modes.
121-
/// Defaults to `false`.
122-
pub trap_on_oom: bool,
123119
/// Resource limiter shared across all stores created from this engine. Defaults to `None`.
124120
pub resource_limiter: Option<Arc<dyn ResourceLimiter>>,
125121
/// Initial number of GC heap bytes that triggers collection.
@@ -171,12 +167,6 @@ impl Config {
171167
self
172168
}
173169

174-
/// Configure whether memory and stack allocation failures trap immediately.
175-
pub fn with_trap_on_oom(mut self, trap_on_oom: bool) -> Self {
176-
self.trap_on_oom = trap_on_oom;
177-
self
178-
}
179-
180170
/// Set the resource limiter shared across all stores created from this engine.
181171
pub fn with_resource_limiter(mut self, limiter: Arc<dyn ResourceLimiter>) -> Self {
182172
self.resource_limiter = Some(limiter);
@@ -193,10 +183,6 @@ impl Config {
193183
pub fn fuel_policy(&self) -> FuelPolicy {
194184
self.fuel_policy
195185
}
196-
197-
pub(crate) const fn trap_on_oom(&self) -> bool {
198-
self.trap_on_oom
199-
}
200186
}
201187

202188
impl Default for Config {
@@ -207,7 +193,6 @@ impl Default for Config {
207193
value_stack_128: StackConfig::fixed(DEFAULT_VALUE_STACK_128_SIZE),
208194
call_stack: StackConfig::fixed(DEFAULT_MAX_CALL_STACK_SIZE),
209195
fuel_policy: FuelPolicy::default(),
210-
trap_on_oom: false,
211196
resource_limiter: None,
212197
gc_collection_threshold: 1024 * 1024,
213198
}
@@ -223,7 +208,6 @@ impl core::fmt::Debug for Config {
223208
.field("value_stack_128", &self.value_stack_128)
224209
.field("call_stack", &self.call_stack)
225210
.field("fuel_policy", &self.fuel_policy)
226-
.field("trap_on_oom", &self.trap_on_oom)
227211
.field("resource_limiter", &self.resource_limiter.is_some())
228212
.field("gc_collection_threshold", &self.gc_collection_threshold)
229213
.finish()

‎crates/tinywasm/src/interpreter/executor.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,11 +1656,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> {
16561656
true => <i64>::stack_pop(&mut self.store.value_stack),
16571657
false => i64::from(<i32>::stack_pop(&mut self.store.value_stack)),
16581658
};
1659-
let trap_on_oom = self.store.engine.config().trap_on_oom();
16601659
let limiter = self.store.engine.config().resource_limiter.as_deref();
16611660

16621661
let mem = self.store.state.get_mem_mut(mem_addr);
1663-
let size = mem.grow(pages_delta, trap_on_oom, limiter)?.unwrap_or(-1);
1662+
let size = mem.grow(pages_delta, limiter)?.unwrap_or(-1);
16641663
match is_64bit {
16651664
true => self.store.value_stack.push::<i64>(size)?,
16661665
false => self.store.value_stack.push::<i32>(size as i32)?,

‎crates/tinywasm/src/lib.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
//! ```
5555
//!
5656
//! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`]
57-
//! and [`engine::Config`] to control stack sizing, fuel accounting, and trap-on-OOM
58-
//! behavior. A [`ResourceLimiter`] can be attached to the engine configuration to bound memory
59-
//! allocation and growth for stores created from that engine.
57+
//! and [`engine::Config`] to control stack sizing and fuel accounting. A [`ResourceLimiter`] can be
58+
//! attached to the engine configuration to bound memory allocation and growth for stores created
59+
//! from that engine and to trap rejected requests.
6060
//!
6161
//! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory.
6262
//!

‎crates/tinywasm/src/reference.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,14 @@ impl Memory {
263263
})
264264
}
265265

266-
/// Grow the memory by the given number of pages.
266+
/// Grows the memory by the given number of pages.
267+
///
268+
/// Returns the previous size, or `None` if growth fails or is rejected by the resource limiter.
269+
/// A limiter-provided trap is returned as an error.
267270
pub fn grow(&self, store: &mut Store, delta_pages: i64) -> Result<Option<i64>> {
268271
let limiter = store.engine.config().resource_limiter.clone();
269272
let mem = self.instance_mut(store)?;
270-
mem.grow(delta_pages, true, limiter.as_deref()).map_err(Into::into)
273+
mem.grow(delta_pages, limiter.as_deref()).map_err(Into::into)
271274
}
272275

273276
/// Get the current size of the memory in pages.

‎crates/tinywasm/src/store/memory/instance.rs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl MemoryInstance {
6868

6969
if initial_len != 0
7070
&& let Some(limiter) = limiter
71-
&& !cold_err!(limiter.memory_growing(0, initial_len, Self::maximum_size(kind)))?
71+
&& !limiter.memory_growing(0, initial_len, Self::maximum_size(kind))?
7272
{
7373
return cold!(Err(Trap::OutOfMemory.into()));
7474
}
@@ -101,7 +101,6 @@ impl MemoryInstance {
101101
pub(crate) fn grow(
102102
&mut self,
103103
pages_delta: i64,
104-
trap_on_oom: bool,
105104
limiter: Option<&dyn ResourceLimiter>,
106105
) -> Result<Option<i64>, Trap> {
107106
let current_pages = self.page_count;
@@ -126,15 +125,12 @@ impl MemoryInstance {
126125
}
127126

128127
if let Some(limiter) = limiter
129-
&& !cold_err!(limiter.memory_growing(self.inner.len(), new_size, Self::maximum_size(self.kind)))?
128+
&& !limiter.memory_growing(self.inner.len(), new_size, Self::maximum_size(self.kind))?
130129
{
131130
return cold!(Ok(None));
132131
}
133132

134-
if let Err(err) = cold_err!(self.inner.grow_to(new_size)) {
135-
if trap_on_oom {
136-
return Err(err);
137-
}
133+
if cold_err!(self.inner.grow_to(new_size)).is_err() {
138134
return Ok(None);
139135
}
140136
self.page_count = new_pages;

‎crates/tinywasm/src/store/mod.rs‎

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,54 @@ pub(crate) use {data::*, element::*, exception::*, function::*, global::*, table
3131
// global store id counter
3232
static STORE_ID: AtomicU32 = AtomicU32::new(0);
3333

34-
/// Limits resource consumption within a [`Store`].
34+
/// Controls resource usage by WebAssembly instances.
3535
///
36-
/// The limiter is consulted before a memory is created or grown, so a host can bound how much a
37-
/// guest may consume. This mirrors the shape of Wasmtime's `ResourceLimiter`; additional resource
38-
/// types can be added as the need arises.
36+
/// Configure a limiter with
37+
/// [`Config::with_resource_limiter`](crate::engine::Config::with_resource_limiter). It currently
38+
/// controls guest linear-memory allocation and growth. It does not account for stacks, GC storage,
39+
/// runtime metadata, or other host allocations.
3940
///
40-
/// A limiter is shared across the stores created from one [`Engine`], so implementations must be
41-
/// `Send + Sync` and use interior mutability to track state.
41+
/// # Example
42+
/// ```rust
43+
/// use std::sync::Arc;
44+
/// use tinywasm::engine::Config;
45+
/// use tinywasm::types::{MemoryArch, MemoryType};
46+
/// use tinywasm::{Engine, Memory, ResourceLimiter, Store};
47+
///
48+
/// struct MemoryLimit(usize);
49+
///
50+
/// impl ResourceLimiter for MemoryLimit {
51+
/// fn memory_growing(
52+
/// &self,
53+
/// _current: usize,
54+
/// desired: usize,
55+
/// _maximum: Option<usize>,
56+
/// ) -> Result<bool, tinywasm::Trap> {
57+
/// Ok(desired <= self.0)
58+
/// }
59+
/// }
60+
///
61+
/// let config = Config::new().with_resource_limiter(Arc::new(MemoryLimit(64 * 1024)));
62+
/// let mut store = Store::new(Engine::new(config));
63+
/// let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?;
64+
/// assert_eq!(memory.grow(&mut store, 1)?, None);
65+
/// # Ok::<(), tinywasm::Error>(())
66+
/// ```
4267
pub trait ResourceLimiter: Send + Sync {
43-
/// Notifies the limiter that a linear memory is about to be allocated or grown.
44-
///
45-
/// `current` and `desired` are byte sizes and are always multiples of the memory's page size.
46-
/// `maximum` is the memory's declared maximum in bytes, or `None` when the memory is unbounded.
68+
/// Returns whether a memory allocation or growth is allowed.
4769
///
48-
/// For initial allocation, `current` is zero. Return `Ok(true)` to allow the allocation or grow,
49-
/// `Ok(false)` to reject it, or `Err` to return the supplied trap. A rejected initial allocation
50-
/// returns [`Trap::OutOfMemory`], while a rejected `memory.grow` returns -1 to the guest.
51-
fn memory_growing(&self, current: usize, desired: usize, maximum: Option<usize>) -> Result<bool, Trap>;
70+
/// Sizes are in bytes. `current` is zero for initial allocation, and `maximum` is `None` for an
71+
/// unbounded memory. `Ok(false)` rejects the request, while `Err` traps. Rejected initial
72+
/// allocations return [`Trap::OutOfMemory`], since they have no normal failure result.
73+
fn memory_growing(
74+
&self,
75+
current: usize,
76+
desired: usize,
77+
maximum: Option<usize>,
78+
) -> core::result::Result<bool, Trap>;
5279
}
5380

54-
/// Global state that can be manipulated by WebAssembly programs
55-
///
56-
/// Managed WebAssembly GC objects are collected automatically. Other Store
57-
/// instances, such as modules, functions, memories, and tables, live until the
58-
/// Store is dropped. GC references exposed through the copyable host value API
59-
/// are retained for the Store's lifetime.
81+
/// Runtime state used by WebAssembly instances and host functions.
6082
///
6183
/// ## Example
6284
/// ```rust

‎crates/tinywasm/tests/memory.rs‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ struct TrapGrowth;
6565

6666
impl ResourceLimiter for TrapGrowth {
6767
fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
68-
if current == 0 { Ok(true) } else { Err(Trap::Other("growth denied")) }
68+
if current == 0 { Ok(true) } else { Err(Trap::Unreachable) }
6969
}
7070
}
7171

@@ -80,16 +80,27 @@ fn resource_limiter_can_reject_growth() -> TestResult {
8080
}
8181

8282
#[test]
83-
fn resource_limiter_can_trap_on_growth() -> TestResult {
84-
let mut store = store_with_limiter(Arc::new(TrapGrowth));
85-
let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?;
83+
fn resource_limiter_rejects_guest_memory_grow() -> TestResult {
84+
let wasm = wat::parse_str(
85+
r#"
86+
(module
87+
(memory 1)
88+
(func (export "grow") (result i32)
89+
i32.const 1
90+
memory.grow))
91+
"#,
92+
)?;
93+
let module = tinywasm::parse_bytes(&wasm)?;
94+
let mut store = store_with_limiter(Arc::new(DenyGrowth));
95+
let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
8696

87-
assert!(matches!(memory.grow(&mut store, 1).unwrap_err(), tinywasm::Error::Trap(Trap::Other("growth denied"))));
97+
let grow = instance.func::<(), i32>(&store, "grow")?;
98+
assert_eq!(grow.call(&mut store, ())?, -1);
8899
Ok(())
89100
}
90101

91102
#[test]
92-
fn resource_limiter_rejects_guest_memory_grow() -> TestResult {
103+
fn resource_limiter_can_trap_guest_memory_grow() -> TestResult {
93104
let wasm = wat::parse_str(
94105
r#"
95106
(module
@@ -100,11 +111,11 @@ fn resource_limiter_rejects_guest_memory_grow() -> TestResult {
100111
"#,
101112
)?;
102113
let module = tinywasm::parse_bytes(&wasm)?;
103-
let mut store = store_with_limiter(Arc::new(DenyGrowth));
114+
let mut store = store_with_limiter(Arc::new(TrapGrowth));
104115
let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
105116

106117
let grow = instance.func::<(), i32>(&store, "grow")?;
107-
assert_eq!(grow.call(&mut store, ())?, -1);
118+
assert!(matches!(grow.call(&mut store, ()), Err(tinywasm::Error::Trap(Trap::Unreachable))));
108119
Ok(())
109120
}
110121

0 commit comments

Comments
 (0)