Skip to content

Commit 64fdb71

Browse files
fix: enforce limits on initial memory allocation, cleanup memory instance
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent af0f6de commit 64fdb71

11 files changed

Lines changed: 124 additions & 137 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 allocation is 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 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`.
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: 4 additions & 3 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 growth.
20+
- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory allocation and growth.
2121

2222
### Changed
2323

@@ -27,13 +27,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`.
2828
- Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution.
2929
- Linear memory now uses a single contiguous `Vec`-backed storage with const-generic fixed-width loads and stores.
30+
- Increased the minimum supported Rust version from 1.95 to 1.98.
3031

3132
### Fixed
3233

3334
- Directly defined imports now reject handles from a different `Store`.
3435
- Tail calls to host functions now return directly to the caller frame.
3536
- Fixed Memory64 bulk-memory operations and optimized stores using the wrong value-stack lane.
36-
- Fixed `memory.init` bounds checks, operand lowering, and local-memory allocation analysis.
37+
- Fixed `memory.init` bounds checks and operand lowering.
3738
- Fixed Memory64 default limits and host-size handling, including 32-bit targets.
3839

3940
### Breaking Changes
@@ -45,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4546
- Renamed `ModuleInstanceAddr` to `ModuleInstanceId`.
4647
- Removed `HostFunction::ty` and `WasmFunction::ty`. Use `Function::ty(&Store)` for runtime function types.
4748
- Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`.
48-
- Removed the pluggable memory backend system (`LinearMemory`, `MemoryBackend`, `VecMemory`, `PagedMemory`, `LazyLinearMemory`, and `Config::with_memory_backend`). Linear memory is always `Vec`-backed.
49+
- 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`.
4950
- Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly.
5051

5152
## [0.10.0] - 2026-07-24

‎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 growth.
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.
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/parser/src/conversion.rs‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,29 +151,26 @@ pub(crate) fn convert_module_code(
151151
ty_idx: u32,
152152
options: &ParserOptions,
153153
) -> Result<(FunctionCode, Option<FuncValidatorAllocations>, OperatorsReaderAllocations)> {
154-
let locals_reader = func.get_locals_reader()?;
155-
#[cfg(feature = "validate")]
156-
let locals_position = locals_reader.original_position();
154+
let mut locals_reader = func.get_locals_reader()?;
157155
let signature = metadata.signature(ty_idx)?.clone();
158156
let mut local_types = signature.params.clone();
159157

160158
#[cfg(feature = "validate")]
161159
let mut validator = validator;
162160

163-
#[cfg(feature = "validate")]
164-
for (local_index, local) in locals_reader.into_iter().enumerate() {
165-
let local = local?;
161+
for _ in 0..locals_reader.get_count() {
162+
#[cfg(feature = "validate")]
163+
let position = locals_reader.original_position();
164+
let local = locals_reader.read()?;
165+
#[cfg(feature = "validate")]
166166
if let Some(validator) = validator.as_mut() {
167-
validator.define_locals(locals_position + local_index as u64, local.0, local.1)?;
167+
validator.define_locals(position, local.0, local.1)?;
168168
}
169169
extend_local_types(&mut local_types, local.0, local.1)?;
170170
}
171171

172172
#[cfg(not(feature = "validate"))]
173-
for local in locals_reader {
174-
let local = local?;
175-
extend_local_types(&mut local_types, local.0, local.1)?;
176-
}
173+
let _ = validator;
177174

178175
// maps a local's address to the index in the type's locals array
179176
let mut local_addr_map = Vec::with_capacity(local_types.len());

‎crates/tinywasm/src/instance.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ impl ModuleInstance {
176176
let imported_funcs = addrs.funcs.len();
177177
addrs.funcs.extend(store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs));
178178
addrs.tags.extend(store.init_tags(&module.tags, &type_addrs));
179-
addrs.memories.extend(store.init_memories(&module.memory_types, MemoryInstance::new)?);
179+
let limiter = store.engine.config().resource_limiter.clone();
180+
addrs
181+
.memories
182+
.extend(store.init_memories(&module.memory_types, |ty| MemoryInstance::new(ty, limiter.as_deref()))?);
180183

181184
store.init_globals(&mut addrs.globals, &module.globals, &addrs.funcs, &type_addrs)?;
182185
addrs.tables.extend(store.init_tables(&module.tables, &addrs.globals, &addrs.funcs, &type_addrs)?);

‎crates/tinywasm/src/lib.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
//!
5656
//! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`]
5757
//! and [`engine::Config`] to control stack sizing, fuel accounting, and trap-on-OOM
58-
//! behavior. A [`ResourceLimiter`] can be attached to a [`Store`] to bound memory growth.
58+
//! behavior. A [`ResourceLimiter`] can be attached to the engine configuration to bound memory
59+
//! allocation and growth for stores created from that engine.
5960
//!
6061
//! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory.
6162
//!

‎crates/tinywasm/src/reference.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ impl Memory {
163163
/// Create a new memory in the given store.
164164
pub fn new(store: &mut Store, ty: MemoryType) -> Result<Self> {
165165
let addr = store.state.memories.len() as MemAddr;
166-
store.state.memories.push(MemoryInstance::new(ty)?);
166+
let limiter = store.engine.config().resource_limiter.clone();
167+
store.state.memories.push(MemoryInstance::new(ty, limiter.as_deref())?);
167168
Ok(Self(StoreItem::new(store.id(), addr)))
168169
}
169170

@@ -230,17 +231,16 @@ impl Memory {
230231

231232
/// Reads up to `dst.len()` bytes from memory and returns the number of bytes read.
232233
///
233-
/// Depending on the configured backend, this may return fewer bytes than requested even when
234-
/// more data is available. Use [`Self::read_exact`] or [`Self::read_vec`] when you need a full
235-
/// range.
234+
/// This returns fewer bytes than requested when the range extends past the end of memory. Use
235+
/// [`Self::read_exact`] or [`Self::read_vec`] when you need a full range.
236236
pub fn read(&self, store: &Store, offset: usize, dst: &mut [u8]) -> Result<usize> {
237237
Ok(self.instance(store)?.inner.read(offset, dst))
238238
}
239239

240240
/// Writes up to `src.len()` bytes into memory and returns the number of bytes written.
241241
///
242-
/// Depending on the configured backend, this may return fewer bytes than requested even when
243-
/// more space is available. Use [`Self::copy_from_slice`] when you need the full slice written.
242+
/// This returns fewer bytes than requested when the range extends past the end of memory. Use
243+
/// [`Self::copy_from_slice`] when you need the full slice written.
244244
pub fn write(&self, store: &mut Store, offset: usize, src: &[u8]) -> Result<usize> {
245245
Ok(self.instance_mut(store)?.inner.write(offset, src))
246246
}

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

Lines changed: 34 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use tinywasm_types::{MemoryArch, MemoryType};
33
use crate::{Error, ResourceLimiter, Result, Trap};
44

55
use super::{MemoryStorage, memory_oob};
6-
use core::hint::cold_path;
76

87
/// A WebAssembly Memory Instance
98
///
@@ -22,25 +21,17 @@ impl core::fmt::Debug for MemoryInstance {
2221
}
2322

2423
impl MemoryInstance {
25-
const COPY_CHUNK_SIZE: usize = 4 * 1024;
26-
24+
#[inline]
2725
fn host_size(kind: MemoryType, pages: u64) -> Result<usize> {
28-
#[cfg(target_pointer_width = "64")]
29-
{
30-
pages
31-
.checked_mul(kind.page_size())
32-
.map(|size| size as usize)
33-
.ok_or(Error::UnsupportedFeature("memory size exceeds the host address space"))
34-
}
26+
let size = pages
27+
.checked_mul(kind.page_size())
28+
.ok_or(Error::UnsupportedFeature("memory size exceeds the host address space"))?;
29+
usize::try_from(size).map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))
30+
}
3531

36-
#[cfg(not(target_pointer_width = "64"))]
37-
{
38-
let page_size = usize::try_from(kind.page_size())
39-
.map_err(|_| Error::UnsupportedFeature("memory page size exceeds the host address space"))?;
40-
let pages = usize::try_from(pages)
41-
.map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?;
42-
pages.checked_mul(page_size).ok_or(Error::UnsupportedFeature("memory size exceeds the host address space"))
43-
}
32+
#[inline]
33+
fn maximum_size(kind: MemoryType) -> Option<usize> {
34+
kind.page_count_max_declared().map(|pages| Self::host_size(kind, pages).unwrap_or(usize::MAX))
4435
}
4536

4637
#[inline(always)]
@@ -66,7 +57,7 @@ impl MemoryInstance {
6657
}
6758
}
6859

69-
pub(crate) fn new(kind: MemoryType) -> Result<Self> {
60+
pub(crate) fn new(kind: MemoryType, limiter: Option<&dyn ResourceLimiter>) -> Result<Self> {
7061
let initial_len = Self::host_size(kind, kind.page_count_initial())?;
7162

7263
crate::log::debug!(
@@ -75,6 +66,13 @@ impl MemoryInstance {
7566
kind.page_size()
7667
);
7768

69+
if initial_len != 0
70+
&& let Some(limiter) = limiter
71+
&& !cold_err!(limiter.memory_growing(0, initial_len, Self::maximum_size(kind)))?
72+
{
73+
return cold!(Err(Trap::OutOfMemory.into()));
74+
}
75+
7876
let storage = MemoryStorage::try_new(initial_len)?;
7977
Ok(Self { kind, inner: storage, page_count: kind.page_count_initial() as usize })
8078
}
@@ -90,47 +88,14 @@ impl MemoryInstance {
9088
src: usize,
9189
len: usize,
9290
) -> Result<(), Trap> {
93-
fn check_range(mem: &MemoryStorage, addr: usize, len: usize) -> Result<(), crate::Trap> {
94-
let Some(end) = addr.checked_add(len) else {
95-
return cold!(Err(memory_oob(addr, len, mem.len())));
96-
};
97-
98-
if end > mem.len() || end < addr {
99-
return cold!(Err(memory_oob(addr, len, mem.len())));
100-
}
101-
Ok(())
102-
}
103-
104-
check_range(&src_memory.inner, src, len)?;
105-
check_range(&self.inner, dst, len)?;
106-
107-
if len == 0 {
108-
return Ok(());
109-
}
110-
111-
let mut buf = [0u8; Self::COPY_CHUNK_SIZE];
112-
let mut copied = 0;
113-
while copied < len {
114-
let chunk_len = buf.len().min(len - copied);
115-
src_memory.inner.read_exact(src + copied, &mut buf[..chunk_len]).ok_or_else(|| {
116-
cold_path();
117-
memory_oob(src + copied, chunk_len, src_memory.inner.len())
118-
})?;
119-
self.inner.write_all(dst + copied, &buf[..chunk_len]).ok_or_else(|| {
120-
cold_path();
121-
memory_oob(dst + copied, chunk_len, self.inner.len())
122-
})?;
123-
copied += chunk_len;
124-
}
125-
91+
src_memory.inner.checked_range(src, len).ok_or_else(|| cold!(memory_oob(src, len, src_memory.inner.len())))?;
92+
self.inner.checked_range(dst, len).ok_or_else(|| cold!(memory_oob(dst, len, self.inner.len())))?;
93+
self.inner.copy_from(dst, &src_memory.inner, src, len);
12694
Ok(())
12795
}
12896

12997
pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result<(), Trap> {
130-
self.inner.copy_within(dst, src, len).ok_or_else(|| {
131-
cold_path();
132-
memory_oob(dst, len, self.inner.len())
133-
})
98+
self.inner.copy_within(dst, src, len).ok_or_else(|| cold!(memory_oob(dst, len, self.inner.len())))
13499
}
135100

136101
pub(crate) fn grow(
@@ -139,50 +104,34 @@ impl MemoryInstance {
139104
trap_on_oom: bool,
140105
limiter: Option<&dyn ResourceLimiter>,
141106
) -> Result<Option<i64>, Trap> {
142-
if pages_delta < 0 {
143-
cold_path();
144-
crate::log::debug!("memory.grow failed: negative delta {}", pages_delta);
145-
return Ok(None);
146-
}
147-
148107
let current_pages = self.page_count;
149-
let Some(pages_delta) = usize::try_from(pages_delta).ok() else {
150-
return Ok(None);
151-
};
152-
let Some(new_pages) = current_pages.checked_add(pages_delta) else {
153-
return Ok(None);
108+
let Some(new_pages) = usize::try_from(pages_delta).ok().and_then(|delta| current_pages.checked_add(delta))
109+
else {
110+
return cold!(Ok(None));
154111
};
155112
let max_pages = self.kind.page_count_max().try_into().unwrap_or(usize::MAX);
156113

157114
if new_pages > max_pages {
158-
cold_path();
159-
crate::log::debug!("memory.grow failed: new_pages={}, max_pages={}", new_pages, max_pages);
160-
return Ok(None);
115+
return cold!({
116+
crate::log::debug!("memory.grow failed: new_pages={}, max_pages={}", new_pages, max_pages);
117+
Ok(None)
118+
});
161119
}
162120

163121
let Ok(new_size) = Self::host_size(self.kind, new_pages as u64) else {
164-
return Ok(None);
122+
return cold!(Ok(None));
165123
};
166124
if new_size == self.inner.len() {
167125
return Ok(i64::try_from(current_pages).ok());
168126
}
169127

170-
if let Some(limiter) = limiter {
171-
let maximum = self.kind.page_count_max_declared().and_then(|pages| Self::host_size(self.kind, pages).ok());
172-
match limiter.memory_growing(self.inner.len(), new_size, maximum) {
173-
Ok(true) => {}
174-
Ok(false) => {
175-
cold_path();
176-
return Ok(None);
177-
}
178-
Err(trap) => {
179-
cold_path();
180-
return Err(trap);
181-
}
182-
}
128+
if let Some(limiter) = limiter
129+
&& !cold_err!(limiter.memory_growing(self.inner.len(), new_size, Self::maximum_size(self.kind)))?
130+
{
131+
return cold!(Ok(None));
183132
}
184133

185-
if let Err(err) = self.inner.grow_to(new_size) {
134+
if let Err(err) = cold_err!(self.inner.grow_to(new_size)) {
186135
if trap_on_oom {
187136
return Err(err);
188137
}

0 commit comments

Comments
 (0)