Skip to content

Commit 9a3d487

Browse files
feat: add table_growing to ResourceLimiter for consistency
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent c8c38bc commit 9a3d487

10 files changed

Lines changed: 132 additions & 36 deletions

File tree

‎ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Linear memory is a contiguous `Vec<u8>` allocation owned by a `MemoryInstance`.
4646

4747
Fixed-width loads and stores use a single const-generic `read_fixed::<N>` / `write_fixed::<N>` pair rather than per-width vtable methods. Scalar operations reduce to an effective-address computation, a bounds check, a slice access, and a `from_le_bytes` / `to_le_bytes` conversion, with out-of-bounds construction kept on cold paths. Bulk operations such as `fill` and `copy_within` map directly to native slice methods.
4848

49-
Memory growth keeps the Wasm page count and limits on `MemoryInstance`. Before the backing storage is allocated or resized, the configured `ResourceLimiter` is consulted so a host can bound guest memory consumption. The limiter is shared across the stores created from one `Engine` and lives behind an `Arc`.
49+
Memory growth keeps the Wasm page count and limits on `MemoryInstance`. Before memory or table backing storage is allocated or resized, the configured `ResourceLimiter` is consulted so a host can bound guest resource consumption. The limiter is shared across the stores created from one `Engine` and lives behind an `Arc`.
5050

5151
For conventional operating systems, a future mmap-backed storage could reserve virtual address space and use guard pages to move more bounds enforcement to the operating system, reducing explicit checks in linear-memory hot paths. This is the same broad approach described in [Wasmtime's linear-memory architecture](https://docs.wasmtime.dev/contributing-architecture.html#linear-memory), where virtual-memory reservations and guard regions eliminate or deduplicate explicit bounds checks.
5252

‎CHANGELOG.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane.
1818
- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules.
1919
- Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size.
20-
- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory allocation and growth.
20+
- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory and table allocation and growth.
2121

2222
### Changed
2323

@@ -48,7 +48,8 @@ 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.
51+
- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory or table allocation or growth request.
52+
- `Table::grow` now returns `Result<Option<usize>>`, matching `Memory::grow`. Growth limits and allocation failures return `None`, while limiter-provided traps return an error.
5253

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

‎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, or the GC collection threshold. A `ResourceLimiter` attached to the engine's config bounds guest memory allocation and growth and can trap rejected requests.
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 and table 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/tinywasm/src/interpreter/executor.rs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,14 +1880,15 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> {
18801880
let arch = self.store.state.get_table(table_addr).kind.arch();
18811881
let n = self.pop_table_operand(arch)?;
18821882
let val = <ValueRef>::stack_pop(&mut self.store.value_stack);
1883+
let limiter = self.store.engine.config().resource_limiter.as_deref();
18831884
let table = self.store.state.get_table_mut(table_addr);
18841885
let sz = table.size();
1885-
let result = table.grow(n, val);
1886-
match (arch, result) {
1887-
(MemoryArch::I32, Ok(())) => self.store.value_stack.push(sz as i32),
1888-
(MemoryArch::I32, Err(_)) => self.store.value_stack.push(-1_i32),
1889-
(MemoryArch::I64, Ok(())) => self.store.value_stack.push(sz as i64),
1890-
(MemoryArch::I64, Err(_)) => self.store.value_stack.push(-1_i64),
1886+
let grew = table.grow(n, val, limiter)?;
1887+
match (arch, grew) {
1888+
(MemoryArch::I32, true) => self.store.value_stack.push(sz as i32),
1889+
(MemoryArch::I32, false) => self.store.value_stack.push(-1_i32),
1890+
(MemoryArch::I64, true) => self.store.value_stack.push(sz as i64),
1891+
(MemoryArch::I64, false) => self.store.value_stack.push(-1_i64),
18911892
}
18921893
}
18931894

‎crates/tinywasm/src/lib.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555
//!
5656
//! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`]
5757
//! 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.
58+
//! attached to the engine configuration to bound memory and table allocation and growth for stores
59+
//! created 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: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,9 @@ impl Table {
373373
return Err(Error::other("host tables cannot use module-relative concrete reference types"));
374374
}
375375
let init = table_value_to_element(&store.state, ty.element_type, init).map_err(Error::from)?;
376+
let limiter = store.engine.config().resource_limiter.clone();
376377
let addr = store.state.tables.len() as TableAddr;
377-
store.state.tables.push(TableInstance::new(ty, init)?);
378+
store.state.tables.push(TableInstance::new(ty, init, limiter.as_deref())?);
378379
Ok(Self(StoreItem::new(store.id(), addr)))
379380
}
380381

@@ -435,15 +436,23 @@ impl Table {
435436
store.state.get_table_mut(self.0.addr).copy_within(dst, src, len)
436437
}
437438

438-
/// Grow the table and return the previous size.
439-
pub fn grow(&self, store: &mut Store, delta: i32, init: WasmValue) -> Result<usize> {
439+
/// Grows the table and returns the previous size.
440+
///
441+
/// Returns `None` if growth fails or is rejected by the resource limiter. A limiter-provided
442+
/// trap is returned as an error.
443+
pub fn grow(&self, store: &mut Store, delta: i32, init: WasmValue) -> Result<Option<usize>> {
440444
self.0.validate_store(store)?;
441445
let table = store.state.get_table(self.0.addr);
442446
let old_size = table.size();
443447
let init = table_value_to_element(&store.state, table.kind.element_type, init)?;
444-
let delta = usize::try_from(delta).map_err(|_| Trap::TableOutOfBounds { offset: 0, len: 1, max: old_size })?;
445-
store.state.get_table_mut(self.0.addr).grow(delta, init)?;
446-
Ok(old_size)
448+
let Ok(delta) = usize::try_from(delta) else {
449+
return Ok(None);
450+
};
451+
let limiter = store.engine.config().resource_limiter.clone();
452+
match store.state.get_table_mut(self.0.addr).grow(delta, init, limiter.as_deref())? {
453+
true => Ok(Some(old_size)),
454+
false => Ok(None),
455+
}
447456
}
448457
}
449458

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ static STORE_ID: AtomicU32 = AtomicU32::new(0);
3535
///
3636
/// Configure a limiter with
3737
/// [`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.
38+
/// controls guest linear-memory and table allocation and growth. It does not account for stacks,
39+
/// GC storage, runtime metadata, or other host allocations.
4040
///
4141
/// # Example
4242
/// ```rust
@@ -69,13 +69,31 @@ pub trait ResourceLimiter: Send + Sync {
6969
///
7070
/// Sizes are in bytes. `current` is zero for initial allocation, and `maximum` is `None` for an
7171
/// unbounded memory. `Ok(false)` rejects the request, while `Err` traps. Rejected initial
72-
/// allocations return [`Trap::OutOfMemory`], since they have no normal failure result.
72+
/// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. The
73+
/// default implementation allows the request.
7374
fn memory_growing(
7475
&self,
75-
current: usize,
76-
desired: usize,
77-
maximum: Option<usize>,
78-
) -> core::result::Result<bool, Trap>;
76+
_current: usize,
77+
_desired: usize,
78+
_maximum: Option<usize>,
79+
) -> core::result::Result<bool, Trap> {
80+
Ok(true)
81+
}
82+
83+
/// Returns whether a table allocation or growth is allowed.
84+
///
85+
/// Sizes are in elements. `current` is zero for initial allocation, and `maximum` is `None` for
86+
/// an unbounded table. `Ok(false)` rejects the request, while `Err` traps. Rejected initial
87+
/// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. The
88+
/// default implementation allows the request.
89+
fn table_growing(
90+
&self,
91+
_current: usize,
92+
_desired: usize,
93+
_maximum: Option<usize>,
94+
) -> core::result::Result<bool, Trap> {
95+
Ok(true)
96+
}
7997
}
8098

8199
/// Runtime state used by WebAssembly instances and host functions.
@@ -281,6 +299,7 @@ impl Store {
281299
type_addrs: &[TypeAddr],
282300
) -> Result<impl ExactSizeIterator<Item = TableAddr>> {
283301
let start = self.state.tables.len() as TableAddr;
302+
let limiter = self.engine.config().resource_limiter.clone();
284303
self.state.tables.reserve_exact(tables.len());
285304
for table in tables {
286305
let init = match &table.init {
@@ -295,7 +314,7 @@ impl Store {
295314
MemoryArch::I32 => TableType::new(element_type, table.ty.size_initial, table.ty.size_max),
296315
MemoryArch::I64 => TableType::new64(element_type, table.ty.size_initial, table.ty.size_max),
297316
};
298-
self.state.tables.push(TableInstance::new(ty, init)?);
317+
self.state.tables.push(TableInstance::new(ty, init, limiter.as_deref())?);
299318
}
300319
Ok(start..start + tables.len() as TableAddr)
301320
}

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Result, Trap, interpreter::ValueRef};
1+
use crate::{ResourceLimiter, Result, Trap, interpreter::ValueRef};
22
use alloc::vec::Vec;
33
use core::ops::Range;
44
use tinywasm_types::*;
@@ -16,11 +16,17 @@ pub(crate) struct TableInstance {
1616

1717
impl TableInstance {
1818
/// Creates a table filled with the given initial reference.
19-
pub(crate) fn new(kind: TableType, init: ValueRef) -> Result<Self> {
19+
pub(crate) fn new(kind: TableType, init: ValueRef, limiter: Option<&dyn ResourceLimiter>) -> Result<Self> {
2020
let size = cold_err!(usize::try_from(kind.size_initial)).map_err(|_| Trap::OutOfMemory)?;
2121
if size > MAX_TABLE_SIZE {
2222
return Err(Trap::OutOfMemory.into());
2323
}
24+
if size != 0
25+
&& let Some(limiter) = limiter
26+
&& !limiter.table_growing(0, size, Self::maximum_size(kind))?
27+
{
28+
return Err(Trap::OutOfMemory.into());
29+
}
2430
let mut elements = Vec::new();
2531
cold_err!(elements.try_reserve_exact(size)).map_err(|_| Trap::OutOfMemory)?;
2632
elements.resize(size, init);
@@ -74,17 +80,38 @@ impl TableInstance {
7480
Ok(())
7581
}
7682

77-
pub(crate) fn grow(&mut self, n: usize, init: ValueRef) -> Result<(), Trap> {
78-
let len = n.checked_add(self.elements.len()).ok_or(Trap::OutOfMemory)?;
83+
pub(crate) fn grow(
84+
&mut self,
85+
n: usize,
86+
init: ValueRef,
87+
limiter: Option<&dyn ResourceLimiter>,
88+
) -> Result<bool, Trap> {
89+
let Some(len) = n.checked_add(self.elements.len()) else {
90+
return Ok(false);
91+
};
7992
let declared_max = self.kind.size_max.and_then(|max| usize::try_from(max).ok()).unwrap_or(usize::MAX);
8093
let max = declared_max.min(MAX_TABLE_SIZE);
8194
if len > max {
82-
return Err(crate::Trap::TableOutOfBounds { offset: len, len: 1, max: self.elements.len() });
95+
return Ok(false);
96+
}
97+
if len == self.elements.len() {
98+
return Ok(true);
99+
}
100+
if let Some(limiter) = limiter
101+
&& !limiter.table_growing(self.elements.len(), len, Self::maximum_size(self.kind))?
102+
{
103+
return Ok(false);
83104
}
84105

85-
cold_err!(self.elements.try_reserve_exact(n)).map_err(|_| Trap::OutOfMemory)?;
106+
if cold_err!(self.elements.try_reserve_exact(n)).is_err() {
107+
return Ok(false);
108+
}
86109
self.elements.resize(len, init);
87-
Ok(())
110+
Ok(true)
111+
}
112+
113+
fn maximum_size(kind: TableType) -> Option<usize> {
114+
kind.size_max.and_then(|maximum| usize::try_from(maximum).ok())
88115
}
89116

90117
pub(crate) fn size(&self) -> usize {

‎crates/tinywasm/tests/internal_refs.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ fn exported_tables_and_globals_have_handle_and_helper_apis() -> Result<(), Box<d
6565
assert_eq!(table.get(&store, 0)?, WasmValue::Ref(tinywasm::types::RefValue::Null));
6666

6767
let old_size = instance.table("t")?.grow(&mut store, 1, tinywasm::types::RefValue::Null.into())?;
68-
assert_eq!(old_size, 1);
68+
assert_eq!(old_size, Some(1));
6969
assert_eq!(instance.table("t")?.size(&store)?, 2);
7070

7171
Ok(())

‎crates/tinywasm/tests/memory.rs‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ extern crate alloc;
33
use alloc::sync::Arc;
44

55
use tinywasm::engine::Config;
6-
use tinywasm::types::{MemoryArch, MemoryType};
7-
use tinywasm::{Engine, Memory, ModuleInstance, ResourceLimiter, Store, Trap};
6+
use tinywasm::types::{MemoryArch, MemoryType, RefType, RefValue, TableType};
7+
use tinywasm::{Engine, Memory, ModuleInstance, ResourceLimiter, Store, Table, Trap};
88

99
type TestResult<T = ()> = Result<T, Box<dyn core::error::Error>>;
1010

@@ -51,6 +51,10 @@ impl ResourceLimiter for DenyAll {
5151
fn memory_growing(&self, _current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
5252
Ok(false)
5353
}
54+
55+
fn table_growing(&self, _current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
56+
Ok(false)
57+
}
5458
}
5559

5660
struct DenyGrowth;
@@ -59,6 +63,10 @@ impl ResourceLimiter for DenyGrowth {
5963
fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
6064
Ok(current == 0)
6165
}
66+
67+
fn table_growing(&self, current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
68+
Ok(current == 0)
69+
}
6270
}
6371

6472
struct TrapGrowth;
@@ -67,6 +75,10 @@ impl ResourceLimiter for TrapGrowth {
6775
fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
6876
if current == 0 { Ok(true) } else { Err(Trap::Unreachable) }
6977
}
78+
79+
fn table_growing(&self, current: usize, _desired: usize, _maximum: Option<usize>) -> Result<bool, Trap> {
80+
if current == 0 { Ok(true) } else { Err(Trap::Unreachable) }
81+
}
7082
}
7183

7284
#[test]
@@ -138,6 +150,33 @@ fn resource_limiter_rejects_module_memory_initial_size() -> TestResult {
138150
Ok(())
139151
}
140152

153+
#[test]
154+
fn resource_limiter_rejects_table_initial_size() {
155+
let mut store = store_with_limiter(Arc::new(DenyAll));
156+
let result = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into());
157+
158+
assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory))));
159+
}
160+
161+
#[test]
162+
fn resource_limiter_can_reject_table_growth() -> TestResult {
163+
let mut store = store_with_limiter(Arc::new(DenyGrowth));
164+
let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?;
165+
166+
assert_eq!(table.grow(&mut store, 1, RefValue::Null.into())?, None);
167+
assert_eq!(table.size(&store)?, 1);
168+
Ok(())
169+
}
170+
171+
#[test]
172+
fn resource_limiter_can_trap_table_growth() -> TestResult {
173+
let mut store = store_with_limiter(Arc::new(TrapGrowth));
174+
let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?;
175+
176+
assert!(matches!(table.grow(&mut store, 1, RefValue::Null.into()), Err(tinywasm::Error::Trap(Trap::Unreachable))));
177+
Ok(())
178+
}
179+
141180
#[test]
142181
fn resource_limiter_allows_guest_memory_grow_by_default() -> TestResult {
143182
let wasm = wat::parse_str(

0 commit comments

Comments
 (0)