Skip to content

Commit 6a7a6af

Browse files
fix: harden instantiation, stream parsing, and host reentrancy
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent dc8e573 commit 6a7a6af

14 files changed

Lines changed: 351 additions & 80 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ This release adds garbage collection and typed function reference support, singl
6161

6262
### Fixed
6363

64+
- Failed instantiation now preserves complete element and data segment mappings for functions exposed through imported tables, and stops data initialization when element initialization traps.
65+
- Stream parsing now bounds read increments instead of allocating declared section sizes up front, and retries interrupted reads.
66+
- Reentrant host calls now enforce a separate depth limit based on the configured call-stack size instead of allowing unbounded native-stack recursion.
6467
- Instantiation now rejects directly defined imports that contain handles from another `Store`.
6568
- Tail calls to host functions now return directly to the caller frame.
6669
- Memory64 bulk-memory operations and optimized stores now use the correct value-stack lanes.

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,17 @@ See the [examples](./examples) directory and [documentation](https://docs.rs/tin
4848

4949
## Cargo Features
5050

51-
- **`full`:** Enables `archive`, `parallel-parser`, `parser`, and `validate`. Enabled by default.
51+
- **`full`:** Enables `archive`, `debug`, `parallel-parser`, `parser`, `state`, and `validate`. Enabled by default.
5252
- **`std`:** Enables `std` and parsing from files and streams. Enabled by default.
5353
- **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default.
5454
- **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through [`ParserOptions`](https://docs.rs/tinywasm/latest/tinywasm/parser/struct.ParserOptions.html).
5555
- **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default.
5656
- **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default.
57-
- **`log`:** Enables integration with the `log` crate. Enabled by default.
57+
- **`log`:** Enables integration with the `log` crate.
5858
- **`send`:** Makes stores and store-local handles movable across threads.
59-
- **`state`:** Adds store-owned host state for access from host callbacks.
59+
- **`state`:** Adds store-owned host state for access from host callbacks. Enabled by default.
6060
- **`portable-atomic`:** Supports targets without native atomic compare-and-swap.
61-
- **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results. Enabled by default.
61+
- **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results.
6262
- **`debug`:** Derives `Debug` for runtime types. Enabled by default.
6363
- **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`).
6464
- **`nightly-tail-calls`:** Uses Rust's unstable explicit tail calls for interpreter dispatch. Requires nightly Rust (recommended for maximum performance).

‎crates/parser/src/lib.rs‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,24 @@ impl Parser {
190190
#[cfg(feature = "std")]
191191
fn read_more(stream: &mut impl std::io::Read, buffer: &mut alloc::vec::Vec<u8>, hint: usize) -> Result<usize> {
192192
let len = buffer.len();
193-
buffer.resize(len + hint, 0);
194-
let read_bytes = stream
195-
.read(&mut buffer[len..])
196-
.map_err(|e| ParseError::Other(alloc::format!("Error reading from stream: {e}")))?;
193+
// Size hints can come from untrusted section lengths.
194+
let increment = hint.clamp(1, 64 * 1024);
195+
let new_len =
196+
len.checked_add(increment).ok_or_else(|| ParseError::Other("stream buffer is too large".into()))?;
197+
buffer
198+
.try_reserve(increment)
199+
.map_err(|e| ParseError::Other(alloc::format!("Error reserving stream buffer: {e}")))?;
200+
buffer.resize(new_len, 0);
201+
let read_bytes = loop {
202+
match stream.read(&mut buffer[len..]) {
203+
Ok(read_bytes) => break read_bytes,
204+
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
205+
Err(e) => {
206+
buffer.truncate(len);
207+
return Err(ParseError::Other(alloc::format!("Error reading from stream: {e}")));
208+
}
209+
}
210+
};
197211
buffer.truncate(len + read_bytes);
198212
Ok(read_bytes)
199213
}

‎crates/tinywasm/src/engine.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ const DEFAULT_MAX_CALL_STACK_SIZE: usize = 1024; // 1024 frames
6868
pub struct StackConfig {
6969
/// Initial reserved capacity for the stack.
7070
pub initial_size: usize,
71-
/// Maximum number of elements the stack may contain.
71+
/// Growth threshold, checked only when the stack reaches its allocated capacity.
72+
/// Dynamic stacks may exceed this threshold up to their allocated capacity.
7273
pub max_size: usize,
7374
/// Whether the stack may grow past its initial capacity.
7475
pub dynamic: bool,
@@ -80,7 +81,9 @@ impl StackConfig {
8081
Self { initial_size: size, max_size: size, dynamic: false }
8182
}
8283

83-
/// Creates a dynamically growing stack with the given initial and maximum sizes.
84+
/// Creates a dynamically growing stack with the given initial size and growth threshold.
85+
///
86+
/// The threshold is not a strict element limit, see [`Self::max_size`].
8487
pub const fn dynamic(initial_size: usize, max_size: usize) -> Self {
8588
assert!(initial_size <= max_size, "initial_size must be less than or equal to max_size");
8689
Self { initial_size, max_size, dynamic: true }

‎crates/tinywasm/src/func/context.rs‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ impl FuncContext<'_> {
8989

9090
let call_stack_base = self.store.call_stack.len();
9191
let value_stack_base = self.store.value_stack.base();
92-
func.call_untyped(self.store, type_addr, args, results, call_stack_base, value_stack_base)
92+
self.with_reentrant_call(|store| {
93+
func.call_untyped(store, type_addr, args, results, call_stack_base, value_stack_base)
94+
})
9395
}
9496

9597
/// Calls a Store-aware function reference in the current module context.
@@ -117,7 +119,23 @@ impl FuncContext<'_> {
117119
func.func.item.validate_store(self.store)?;
118120
let call_stack_base = self.store.call_stack.len();
119121
let value_stack_base = self.store.value_stack.base();
120-
func.func.call_typed(self.store, params.into_wasm_values(), call_stack_base, value_stack_base)
122+
self.with_reentrant_call(|store| {
123+
func.func.call_typed(store, params.into_wasm_values(), call_stack_base, value_stack_base)
124+
})
125+
}
126+
127+
fn with_reentrant_call<R>(&mut self, call: impl FnOnce(&mut crate::Store) -> Result<R>) -> Result<R> {
128+
// Each nested executor keeps its current frame on the native stack,
129+
// outside CallStack, even when the Wasm function has no params or locals.
130+
let config = self.store.engine.config().call_stack;
131+
let limit = if config.dynamic { config.max_size } else { config.initial_size };
132+
if self.store.reentrant_call_depth >= limit {
133+
return Err(crate::Trap::CallStackOverflow.into());
134+
}
135+
self.store.reentrant_call_depth += 1;
136+
let result = call(self.store);
137+
self.store.reentrant_call_depth -= 1;
138+
result
121139
}
122140
}
123141

‎crates/tinywasm/src/instance.rs‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,8 @@ impl ModuleInstance {
189189

190190
store.init_globals(&mut addrs.globals, &module.globals, &addrs.funcs, &type_addrs)?;
191191
addrs.tables.extend(store.init_tables(&module.tables, &addrs.globals, &addrs.funcs, &type_addrs)?);
192-
let (elem_addrs, elem_trapped) =
193-
store.init_elements(&addrs.tables, &addrs.funcs, &addrs.globals, &module.elements, &type_addrs)?;
194-
let (data_addrs, data_trapped) =
195-
store.init_data(&addrs.memories, &addrs.globals, &addrs.funcs, &module.data, &type_addrs)?;
192+
let elem_addrs = store.alloc_elements(&addrs.funcs, &addrs.globals, &module.elements, &type_addrs)?;
193+
let data_addrs = store.alloc_data(&module.data);
196194

197195
let instance = ModuleInstanceInner {
198196
store_id: store.id(),
@@ -210,11 +208,25 @@ impl ModuleInstance {
210208
};
211209

212210
let instance = ModuleInstance(StoreShared::new(instance));
211+
// Earlier table writes can expose functions even if a later segment traps.
213212
store.add_instance(instance.clone());
214213

215-
if let Some(trap) = elem_trapped.or(data_trapped) {
216-
return cold!(Err(trap.into()));
217-
}
214+
store.init_elements(
215+
&instance.0.table_addrs,
216+
&instance.0.func_addrs,
217+
&instance.0.global_addrs,
218+
&module.elements,
219+
&instance.0.type_addrs,
220+
&instance.0.elem_addrs,
221+
)?;
222+
store.init_data(
223+
&instance.0.mem_addrs,
224+
&instance.0.global_addrs,
225+
&instance.0.func_addrs,
226+
&module.data,
227+
&instance.0.type_addrs,
228+
&instance.0.data_addrs,
229+
)?;
218230
Ok(instance)
219231
}
220232

‎crates/tinywasm/src/interpreter/stack/call_stack.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ impl CallStack {
3737

3838
#[inline(always)]
3939
pub(crate) fn push(&mut self, mut call_frame: CallFrame, instr_ptr: usize) -> Result<(), Trap> {
40+
// Check the limit only at capacity to avoid an extra hot-path check. Vec growth may
41+
// intentionally overshoot max_size. Revisit when Vec::push_within_capacity is stable.
4042
if self.stack.len() == self.stack.capacity() && (!self.dynamic || self.stack.len() >= self.max_size) {
4143
return cold!(Err(Trap::CallStackOverflow));
4244
}

‎crates/tinywasm/src/interpreter/stack/value_stack.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ impl<T: Copy + Default> Stack<T> {
4646

4747
#[inline(always)]
4848
pub(crate) fn push(&mut self, value: T) -> Result<(), Trap> {
49+
// Check the limit only at capacity to avoid an extra hot-path check. Vec growth may
50+
// intentionally overshoot max_size. Revisit when Vec::push_within_capacity is stable.
4951
if self.data.len() == self.data.capacity() && (!self.dynamic || self.data.len() >= self.max_size) {
5052
return cold!(Err(Trap::ValueStackOverflow));
5153
}
@@ -55,6 +57,7 @@ impl<T: Copy + Default> Stack<T> {
5557

5658
#[inline(always)]
5759
pub(crate) fn push_copy(&mut self, index: usize) -> Result<(), Trap> {
60+
// Keep the same capacity-based limit check as push, including intentional overshoot.
5861
if self.data.len() == self.data.capacity() && (!self.dynamic || self.data.len() >= self.max_size) {
5962
return cold!(Err(Trap::ValueStackOverflow));
6063
}

‎crates/tinywasm/src/lib.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,17 @@
6565
//!
6666
//! ## Cargo features
6767
//!
68-
//! - **`full`:** Enables `archive`, `parallel-parser`, `parser`, and `validate`. Enabled by default.
68+
//! - **`full`:** Enables `archive`, `debug`, `parallel-parser`, `parser`, `state`, and `validate`. Enabled by default.
6969
//! - **`std`:** Enables `std` and parsing from files and streams. Enabled by default.
7070
//! - **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default.
7171
//! - **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through `ParserOptions`.
7272
//! - **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default.
7373
//! - **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default.
74-
//! - **`log`:** Enables integration with the `log` crate. Enabled by default.
74+
//! - **`log`:** Enables integration with the `log` crate.
7575
//! - **`send`:** Makes stores and store-local handles movable across threads.
76+
//! - **`state`:** Adds store-owned host state for access from host callbacks. Enabled by default.
7677
//! - **`portable-atomic`:** Supports targets without native atomic compare-and-swap.
77-
//! - **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results. Enabled by default.
78+
//! - **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results.
7879
//! - **`debug`:** Derives `Debug` for runtime types. Enabled by default.
7980
//! - **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`).
8081
//! - **`nightly-tail-calls`:** Uses Rust's unstable explicit tail calls for interpreter dispatch. Requires nightly Rust (recommended for maximum performance).

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,7 @@ impl MemoryInstance {
101101
src: usize,
102102
len: usize,
103103
) -> Result<(), Trap> {
104-
cold_err!(src_mem.inner.checked_range(src, len).ok_or_else(|| memory_oob(src, len, src_mem.inner.len())))?;
105-
cold_err!(self.inner.checked_range(dst, len).ok_or_else(|| memory_oob(dst, len, self.inner.len())))?;
106-
cold_err!(self.inner.copy_from(dst, &src_mem.inner, src, len).ok_or_else(|| memory_oob(
107-
dst,
108-
len,
109-
self.inner.len()
110-
)))?;
111-
Ok(())
104+
self.inner.copy_from(dst, &src_mem.inner, src, len)
112105
}
113106

114107
pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result<(), Trap> {

0 commit comments

Comments
 (0)