From f4db548a7cd1fbe0df2a6cd5f549616c2ad15be5 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 17 Aug 2026 21:31:08 +0200 Subject: [PATCH 1/5] chore: remove LinearMemory trait, add mem0 cache Signed-off-by: Henry --- ARCHITECTURE.md | 23 +- CHANGELOG.md | 5 +- Cargo.toml | 4 - README.md | 2 +- benches/memory_backends.rs | 151 ------- crates/cli/src/engine_flags.rs | 28 +- crates/parser/src/lib.rs | 20 +- crates/parser/src/module.rs | 39 +- crates/parser/src/parallel.rs | 29 +- crates/tinywasm/src/engine.rs | 48 ++- crates/tinywasm/src/instance.rs | 24 +- crates/tinywasm/src/interpreter/executor.rs | 95 ++--- crates/tinywasm/src/lib.rs | 6 +- crates/tinywasm/src/reference.rs | 14 +- crates/tinywasm/src/store/memory/instance.rs | 53 ++- crates/tinywasm/src/store/memory/lazy.rs | 114 ------ crates/tinywasm/src/store/memory/mod.rs | 390 +------------------ crates/tinywasm/src/store/memory/paged.rs | 368 ----------------- crates/tinywasm/src/store/memory/vec.rs | 181 +++------ crates/tinywasm/src/store/mod.rs | 26 +- crates/tinywasm/tests/memory.rs | 121 ++++++ crates/tinywasm/tests/memory_backends.rs | 307 --------------- crates/types/src/instructions.rs | 83 ---- crates/types/src/lib.rs | 23 +- examples/rust/Cargo.toml | 14 +- examples/rust/build.sh | 4 +- examples/rust/src/hello.rs | 12 +- examples/rust/src/print.rs | 4 +- examples/rust/src/print.twasm | Bin 154 -> 154 bytes examples/rust/src/tinywasm_precompiled.rs | 30 -- examples/wasm-rust.rs | 24 -- 31 files changed, 404 insertions(+), 1838 deletions(-) delete mode 100644 benches/memory_backends.rs delete mode 100644 crates/tinywasm/src/store/memory/lazy.rs delete mode 100644 crates/tinywasm/src/store/memory/paged.rs create mode 100644 crates/tinywasm/tests/memory.rs delete mode 100644 crates/tinywasm/tests/memory_backends.rs delete mode 100644 examples/rust/src/tinywasm_precompiled.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9405d380..8e78ef58 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # TinyWasm Architecture -TinyWasm follows the general runtime model described in the [WebAssembly specification](https://webassembly.github.io/spec/core/exec/runtime.html). It is a stack-based interpreter with a compact internal bytecode, width-specific value stacks, and configurable linear-memory backends. +TinyWasm follows the general runtime model described in the [WebAssembly specification](https://webassembly.github.io/spec/core/exec/runtime.html). It is a stack-based interpreter with a compact internal bytecode, width-specific value stacks, and a contiguous `Vec`-backed linear memory. ## Execution Pipeline @@ -40,29 +40,20 @@ The default runtime remains safe Rust throughout rather than relying on unchecke SIMD instructions have a portable safe-Rust implementation built from fixed-size arrays and lane operations, relying on the compiler to auto-vectorize where possible. Generated code is inspected with `cargo asm`, and benchmarks determine where architecture-specific alternatives are worthwhile. WebAssembly targets use native SIMD intrinsics where available, while the optional `simd-x86` feature provides selected x86 implementations for operations where the generic code produces worse results. -## Memory Backends +## Linear Memory -Linear memory is implemented through the `LinearMemory` trait. The backend is selected with `engine::Config::with_memory_backend()`. +Linear memory is a contiguous `Vec` allocation owned by a `MemoryInstance`. The interpreter accesses it through the internal `MemoryStorage` type, a small concrete boundary that keeps the `Vec` representation out of the executor so an mmap-backed storage can be substituted later without touching load and store paths. -`LinearMemory` exposes separate fixed-width read and write methods for 8-, 16-, 32-, 64-, and 128-bit accesses. A const-generic method would not be callable through a `dyn LinearMemory` trait object, so each width is an explicit vtable entry that backends can optimize independently. +Fixed-width loads and stores use a single const-generic `read_fixed::` / `write_fixed::` 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. -This flexibility has a measurable cost: guest loads and stores cross the `dyn LinearMemory` boundary, adding an indirect call and generally preventing the backend operation from being inlined into the interpreter. The fixed-width methods keep the work behind that boundary as small and specialized as possible. +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`. -Available backends: - -- `VecMemory` - contiguous `Vec` backing and the default backend. -- `PagedMemory` - sparse chunk-based allocation, with untouched chunks left unallocated and growth avoiding relocation of one contiguous buffer. -- `LazyLinearMemory` - serves zero-filled reads without allocation and creates the configured backend on the first mutation or growth. -- Custom backends through `MemoryBackend::custom()`. - -`VecMemory` growth may reallocate, though operating-system allocators can often grow page-backed allocations without copying the full buffer. Applications on conventional operating systems should generally keep it unless sparse allocation or non-relocating growth is specifically needed. Bounded dynamic stacks and sparse paged memory trade some runtime overhead for a smaller initial footprint on embedded and other resource-constrained systems. +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. ## Future Experiments Future work may explore additional dispatch and code-generation strategies, including Rust's experimental `loop_match` state-machine work, a tail-call-based interpreter once Rust's explicit tail-call support matures, more aggressive superinstruction fusion, top-of-stack register allocation, or optional JIT compilation. -For conventional operating systems, a future `mmap`-based memory backend 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. - ## Important Modules - [visit.rs](./crates/parser/src/visit.rs) - function-body operator lowering @@ -71,4 +62,4 @@ For conventional operating systems, a future `mmap`-based memory backend could r - [instructions.rs](./crates/types/src/instructions.rs) - internal instruction set - [value_stack.rs](./crates/tinywasm/src/interpreter/stack/value_stack.rs) - width-specific stacks - [call_stack.rs](./crates/tinywasm/src/interpreter/stack/call_stack.rs) - call frame stack -- [memory/mod.rs](./crates/tinywasm/src/store/memory/mod.rs) - memory backend trait and implementations +- [memory/mod.rs](./crates/tinywasm/src/store/memory/mod.rs) - linear memory storage diff --git a/CHANGELOG.md b/CHANGELOG.md index 701fc843..d0ee0213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane. - Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules. - Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size. +- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory growth. ### Changed @@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Typed function tuples now support up to 20 parameters or results. `WasmTupleChain` is deprecated. Use untyped functions for larger signatures. - Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`. - Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution. -- `LinearMemory` mutation methods and custom memory-backend factories now return `Trap` errors so lazy backend failures can be propagated. +- Linear memory now uses a single contiguous `Vec`-backed storage with const-generic fixed-width loads and stores. ### Fixed @@ -44,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `ModuleInstanceAddr` to `ModuleInstanceId`. - Removed `HostFunction::ty` and `WasmFunction::ty`. Use `Function::ty(&Store)` for runtime function types. - Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`. +- Removed the pluggable memory backend system (`LinearMemory`, `MemoryBackend`, `VecMemory`, `PagedMemory`, `LazyLinearMemory`, and `Config::with_memory_backend`). Linear memory is always `Vec`-backed. +- Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly. ## [0.10.0] - 2026-07-24 diff --git a/Cargo.toml b/Cargo.toml index 7ea51814..e678b1e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,10 +57,6 @@ name = "tinywasm" harness = false name = "tinywasm_modes" -[[bench]] -harness = false -name = "memory_backends" - [dev-dependencies] anyhow.workspace = true criterion.workspace = true diff --git a/README.md b/README.md index 3d3bc4c0..24d49367 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ TinyWasm modules can be compiled to the internal `twasm` bytecode format, which With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments. -Use `Engine` and `engine::Config` when you need non-default runtime settings such as fuel accounting, stack sizing, memory backend selection, the GC collection threshold, or trap-on-OOM behavior. +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. [^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`. diff --git a/benches/memory_backends.rs b/benches/memory_backends.rs deleted file mode 100644 index d7b8703f..00000000 --- a/benches/memory_backends.rs +++ /dev/null @@ -1,151 +0,0 @@ -use criterion::measurement::WallTime; -use criterion::{BatchSize, BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main}; -use std::hint::black_box; -use tinywasm::{LinearMemory, PagedMemory, VecMemory}; - -const PAGE_SIZE: usize = 64 * 1024; -const CHUNK_SIZE: usize = 4 * 1024; -const GROW_STEPS: usize = 32; -const BENCH_MEASUREMENT_TIME: std::time::Duration = std::time::Duration::from_secs(10); - -const MEMORY_LEN: usize = PAGE_SIZE * 4; -const CONTIGUOUS_OFFSET: usize = 1024; -const CONTIGUOUS_LEN: usize = 2048; -const CROSS_CHUNK_OFFSET: usize = CHUNK_SIZE - 512; -const CROSS_CHUNK_LEN: usize = CHUNK_SIZE * 2; - -fn bench_grow(group: &mut BenchmarkGroup<'_, WallTime>, backend: &str, make_memory: F) -where - M: LinearMemory, - F: Fn() -> M + Copy, -{ - group.bench_function(BenchmarkId::new("grow", backend), |b| { - b.iter_batched( - make_memory, - |mut memory| { - for page_count in 2..=GROW_STEPS + 1 { - memory.grow_to(page_count * PAGE_SIZE).unwrap(); - } - black_box(memory.len()) - }, - BatchSize::SmallInput, - ) - }); -} - -fn bench_write_all( - group: &mut BenchmarkGroup<'_, WallTime>, - backend: &str, - workload: &str, - mut memory: M, - offset: usize, - len: usize, -) { - let src = vec![0xA5; len]; - group.bench_function(BenchmarkId::new(format!("write_all/{workload}"), backend), |b| { - b.iter(|| { - memory.write_all(offset, black_box(&src)).unwrap(); - black_box(memory.len()) - }) - }); -} - -fn bench_read_exact( - group: &mut BenchmarkGroup<'_, WallTime>, - backend: &str, - workload: &str, - mut memory: M, - offset: usize, - len: usize, -) { - let src = vec![0x5A; len]; - memory.write_all(offset, &src).unwrap(); - - let mut dst = vec![0; len]; - group.bench_function(BenchmarkId::new(format!("read_exact/{workload}"), backend), |b| { - b.iter(|| { - memory.read_exact(offset, black_box(&mut dst)).unwrap(); - black_box(&dst); - }) - }); -} - -fn criterion_benchmark(c: &mut Criterion) { - let mut group = c.benchmark_group("memory_backends"); - group.measurement_time(BENCH_MEASUREMENT_TIME); - - bench_grow(&mut group, "vec", || VecMemory::try_new(PAGE_SIZE).expect("bench memory should be constructible")); - bench_grow(&mut group, "paged", || { - PagedMemory::try_new(PAGE_SIZE, CHUNK_SIZE).expect("bench memory should be constructible") - }); - bench_write_all( - &mut group, - "vec", - "contiguous", - VecMemory::try_new(MEMORY_LEN).expect("bench memory should be constructible"), - CONTIGUOUS_OFFSET, - CONTIGUOUS_LEN, - ); - bench_write_all( - &mut group, - "paged", - "contiguous", - PagedMemory::try_new(MEMORY_LEN, CHUNK_SIZE).expect("bench memory should be constructible"), - CONTIGUOUS_OFFSET, - CONTIGUOUS_LEN, - ); - bench_read_exact( - &mut group, - "vec", - "contiguous", - VecMemory::try_new(MEMORY_LEN).expect("bench memory should be constructible"), - CONTIGUOUS_OFFSET, - CONTIGUOUS_LEN, - ); - bench_read_exact( - &mut group, - "paged", - "contiguous", - PagedMemory::try_new(MEMORY_LEN, CHUNK_SIZE).expect("bench memory should be constructible"), - CONTIGUOUS_OFFSET, - CONTIGUOUS_LEN, - ); - - bench_write_all( - &mut group, - "vec", - "cross_chunk", - VecMemory::try_new(MEMORY_LEN).expect("bench memory should be constructible"), - CROSS_CHUNK_OFFSET, - CROSS_CHUNK_LEN, - ); - bench_write_all( - &mut group, - "paged", - "cross_chunk", - PagedMemory::try_new(MEMORY_LEN, CHUNK_SIZE).expect("bench memory should be constructible"), - CROSS_CHUNK_OFFSET, - CROSS_CHUNK_LEN, - ); - bench_read_exact( - &mut group, - "vec", - "cross_chunk", - VecMemory::try_new(MEMORY_LEN).expect("bench memory should be constructible"), - CROSS_CHUNK_OFFSET, - CROSS_CHUNK_LEN, - ); - bench_read_exact( - &mut group, - "paged", - "cross_chunk", - PagedMemory::try_new(MEMORY_LEN, CHUNK_SIZE).expect("bench memory should be constructible"), - CROSS_CHUNK_OFFSET, - CROSS_CHUNK_LEN, - ); - - group.finish(); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/crates/cli/src/engine_flags.rs b/crates/cli/src/engine_flags.rs index 3d90847e..9c5c068b 100644 --- a/crates/cli/src/engine_flags.rs +++ b/crates/cli/src/engine_flags.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, bail}; +use anyhow::Result; use clap::{Args, ValueEnum}; use tinywasm::{Engine, StackConfig, engine::FuelPolicy}; @@ -12,14 +12,6 @@ pub struct EngineFlags { #[arg(long)] pub trap_on_oom: bool, - /// Memory backend to use for instantiated memories - #[arg(long, value_enum)] - pub memory_backend: Option, - - /// Chunk size in bytes for the paged memory backend - #[arg(long, default_value_t = 64 * 1024)] - pub memory_page_chunk_size: usize, - /// Fixed value stack size for all value lanes #[arg(long, conflicts_with = "value_stack_dynamic")] pub value_stack_size: Option, @@ -43,12 +35,6 @@ pub enum FuelPolicyArg { Weighted, } -#[derive(Clone, Copy, ValueEnum)] -pub enum MemoryBackendArg { - Vec, - Paged, -} - #[derive(Clone)] pub struct StackSpec { initial: usize, @@ -86,18 +72,6 @@ impl EngineFlags { }); } - if let Some(memory_backend) = self.memory_backend { - config = config.with_memory_backend(match memory_backend { - MemoryBackendArg::Vec => tinywasm::MemoryBackend::vec(), - MemoryBackendArg::Paged => { - if self.memory_page_chunk_size == 0 { - bail!("--memory-page-chunk-size must be greater than zero"); - } - tinywasm::MemoryBackend::paged(self.memory_page_chunk_size) - } - }); - } - if let Some(value_stack_size) = self.value_stack_size { config = config.with_value_stack(StackConfig::fixed(value_stack_size)); } diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 90e239bd..fbe53274 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -61,8 +61,6 @@ pub struct ParserOptions { /// Disable this only for trusted input. Parsing without validation may produce /// a module that violates runtime assumptions. pub validation: bool, - /// Whether to optimize local memory allocation by skipping allocation of unused local memories. - pub optimize_local_memory_allocation: bool, /// Whether to run the peephole rewrite optimizer. pub optimize_rewrite: bool, /// Whether to deduplicate immutable function operands while parsing. @@ -85,7 +83,6 @@ impl Default for ParserOptions { fn default() -> Self { Self { validation: cfg!(feature = "validate"), - optimize_local_memory_allocation: true, optimize_rewrite: true, deduplicate_operands: false, #[cfg(parallel_parser)] @@ -117,17 +114,6 @@ impl ParserOptions { self.validation } - /// Enable or disable the optimization that skips allocating unused local memories. - pub const fn with_local_memory_allocation_optimization(mut self, enabled: bool) -> Self { - self.optimize_local_memory_allocation = enabled; - self - } - - /// Returns whether unused local memory allocation optimization is enabled. - pub const fn optimize_local_memory_allocation(&self) -> bool { - self.optimize_local_memory_allocation - } - /// Enable or disable the peephole rewrite optimizer. pub const fn with_rewrite_optimization(mut self, enabled: bool) -> Self { self.optimize_rewrite = enabled; @@ -234,7 +220,7 @@ impl Parser { } reader.process_pending_functions(&self.options)?; - reader.into_module(&self.options) + reader.into_module() } #[cfg(feature = "std")] @@ -335,7 +321,7 @@ impl Parser { if reader.end_reached || eof { reader.process_pending_functions(&self.options)?; - return reader.into_module(&self.options); + return reader.into_module(); } } }; @@ -347,7 +333,7 @@ impl TryFrom> for Module { type Error = ParseError; fn try_from(reader: ModuleReader<'_>) -> Result { - reader.into_module(&ParserOptions::default()) + reader.into_module() } } diff --git a/crates/parser/src/module.rs b/crates/parser/src/module.rs index 81386662..876dc677 100644 --- a/crates/parser/src/module.rs +++ b/crates/parser/src/module.rs @@ -19,7 +19,6 @@ pub(crate) struct OptimizedFunctionCode { pub instructions: Vec, pub data: WasmFunctionData, pub locals: ValueCounts, - pub uses_local_memory: bool, } pub(crate) fn optimize_function_code( @@ -27,16 +26,11 @@ pub(crate) fn optimize_function_code( options: &ParserOptions, function_results: ValueCounts, self_func_addr: u32, - imported_memory_count: u32, ) -> Result { let optimized = optimize::optimize_instructions(code.instructions, &mut code.data, options, function_results, self_func_addr)?; let data = code.data.finish(); - let uses_local_memory = optimized - .instructions - .iter() - .any(|instruction| instruction.memory_addr(&data).is_some_and(|memory| memory >= imported_memory_count)); - Ok(OptimizedFunctionCode { instructions: optimized.instructions, data, locals: code.locals, uses_local_memory }) + Ok(OptimizedFunctionCode { instructions: optimized.instructions, data, locals: code.locals }) } #[derive(Default)] @@ -65,7 +59,6 @@ pub(crate) struct ModuleReader<'a> { pub(crate) elements: Box<[Element]>, pub(crate) end_reached: bool, imported_func_count: usize, - imported_memory_count: u32, global_types: Vec, #[cfg(parallel_parser)] @@ -277,7 +270,6 @@ impl<'a> ModuleReader<'a> { } self.imported_func_count += 1; } - ImportKind::Memory(_) => self.imported_memory_count += 1, ImportKind::Global(ty) => self.global_types.push(ty.ty), ImportKind::Tag(tag) => { let ty = self.types.get(tag.type_idx).and_then(SubType::as_func).ok_or_else(|| { @@ -411,7 +403,6 @@ impl<'a> ModuleReader<'a> { options, self.code_results[self.code.len()], (self.imported_func_count + self.code.len()) as u32, - self.imported_memory_count, )?); self.func_validator_allocations = func_validator_allocs; @@ -512,10 +503,8 @@ impl<'a> ModuleReader<'a> { }; let imported_func_count = self.imported_func_count; - let imported_memory_count = self.imported_memory_count; let metadata = self.translation_metadata(); - let code = - crate::parallel::process_pending(pending, metadata, options, imported_func_count, imported_memory_count)?; + let code = crate::parallel::process_pending(pending, metadata, options, imported_func_count)?; self.code.extend(code); Ok(()) } @@ -525,7 +514,7 @@ impl<'a> ModuleReader<'a> { Ok(()) } - pub(crate) fn into_module(self, options: &ParserOptions) -> Result { + pub(crate) fn into_module(self) -> Result { if !self.end_reached { return Err(ParseError::EndNotReached); } @@ -534,24 +523,6 @@ impl<'a> ModuleReader<'a> { return Err(ParseError::Other("Code and code type address count mismatch".to_string())); } - let import_mem_count = self.imported_memory_count; - let has_local_mem_export = - self.exports.iter().any(|export| export.kind == ExternalKind::Memory && export.index >= import_mem_count); - let has_active_data_segment_on_local_memory = self.data.iter().any(|data| match &data.kind { - DataKind::Active { mem, .. } => *mem >= import_mem_count, - DataKind::Passive => false, - }); - let optimize_local_memory_allocation = options.optimize_local_memory_allocation(); - let mut local_memory_allocation = if self.memory_types.is_empty() { - LocalMemoryAllocation::Skip - } else if !optimize_local_memory_allocation || has_active_data_segment_on_local_memory { - LocalMemoryAllocation::Eager - } else if has_local_mem_export { - LocalMemoryAllocation::Lazy - } else { - LocalMemoryAllocation::Skip - }; - let func_type_idxs = self .imports .iter() @@ -571,9 +542,6 @@ impl<'a> ModuleReader<'a> { let ty = self.types.get(ty_idx).and_then(SubType::as_func).expect("function type was checked while parsing"); let params = ValueCounts::from_iter(ty.params()); - if code.uses_local_memory { - local_memory_allocation = LocalMemoryAllocation::Eager; - } Ok(Arc::new(WasmFunction { instructions: code.instructions.into_boxed_slice(), @@ -598,7 +566,6 @@ impl<'a> ModuleReader<'a> { elements: self.elements, memory_types: self.memory_types, tags: self.tags, - local_memory_allocation, } .into()) } diff --git a/crates/parser/src/parallel.rs b/crates/parser/src/parallel.rs index e29b38f4..61051dce 100644 --- a/crates/parser/src/parallel.rs +++ b/crates/parser/src/parallel.rs @@ -61,7 +61,6 @@ fn process_function_job( metadata: &crate::visit::ModuleMetadata, options: &ParserOptions, imported_func_count: usize, - imported_memory_count: u32, validator_allocs: Option, reader_allocs: OperatorsReaderAllocations, ) -> Result<(OptimizedFunctionCode, Option, OperatorsReaderAllocations)> { @@ -83,13 +82,7 @@ fn process_function_job( } }; - let code = optimize_function_code( - code, - options, - job.results, - (imported_func_count + job.ordinal) as u32, - imported_memory_count, - )?; + let code = optimize_function_code(code, options, job.results, (imported_func_count + job.ordinal) as u32)?; Ok((code, validator_allocs, reader_allocs)) } @@ -99,7 +92,6 @@ fn process_chunk<'a>( metadata: &crate::visit::ModuleMetadata, options: &ParserOptions, imported_func_count: usize, - imported_memory_count: u32, ) -> Result> { let mut validator_allocs = None; let mut reader_allocs = OperatorsReaderAllocations::default(); @@ -107,15 +99,8 @@ fn process_chunk<'a>( let mut codes = Vec::with_capacity(jobs.size_hint().0); for job in jobs { - let (code, next_validator_allocs, next_reader_allocs) = process_function_job( - job, - metadata, - options, - imported_func_count, - imported_memory_count, - validator_allocs, - reader_allocs, - )?; + let (code, next_validator_allocs, next_reader_allocs) = + process_function_job(job, metadata, options, imported_func_count, validator_allocs, reader_allocs)?; codes.push(code); validator_allocs = next_validator_allocs; reader_allocs = next_reader_allocs; @@ -129,11 +114,10 @@ pub(crate) fn process_pending( metadata: &crate::visit::ModuleMetadata, options: &ParserOptions, imported_func_count: usize, - imported_memory_count: u32, ) -> Result> { let num_workers = worker_count(options, pending.len()); if num_workers == 1 { - return process_chunk(pending, metadata, options, imported_func_count, imported_memory_count); + return process_chunk(pending, metadata, options, imported_func_count); } let code_count = pending.len(); let chunk_size = pending.len().div_ceil(num_workers); @@ -151,10 +135,7 @@ pub(crate) fn process_pending( bytes += body_len(&job.body); chunk.push(job); } - handles - .push(scope.spawn(move || { - process_chunk(chunk, metadata, options, imported_func_count, imported_memory_count) - })); + handles.push(scope.spawn(move || process_chunk(chunk, metadata, options, imported_func_count))); } let mut codes = Vec::with_capacity(code_count); diff --git a/crates/tinywasm/src/engine.rs b/crates/tinywasm/src/engine.rs index 64ef6be9..b394d0f7 100644 --- a/crates/tinywasm/src/engine.rs +++ b/crates/tinywasm/src/engine.rs @@ -1,5 +1,6 @@ -/// Memory backend types and traits. -pub use crate::store::{LazyLinearMemory, LinearMemory, MemoryBackend, PagedMemory, VecMemory}; +use alloc::sync::Arc; + +use crate::ResourceLimiter; /// Global configuration for the WebAssembly interpreter /// @@ -88,7 +89,7 @@ impl StackConfig { /// /// ## Example /// ```rust -/// use tinywasm::engine::{Config, FuelPolicy, MemoryBackend, StackConfig}; +/// use tinywasm::engine::{Config, FuelPolicy, StackConfig}; /// /// let config = Config::new() /// .with_fuel_policy(FuelPolicy::Weighted) @@ -96,13 +97,11 @@ impl StackConfig { /// .with_value_stack_64(StackConfig::dynamic(1024, 32 * 1024)) /// .with_value_stack_128(StackConfig::dynamic(256, 4 * 1024)) /// .with_call_stack(StackConfig::dynamic(64, 1024)) -/// .with_memory_backend(MemoryBackend::paged(64 * 1024)) /// .with_trap_on_oom(true); /// /// assert!(matches!(config.fuel_policy(), FuelPolicy::Weighted)); /// ``` #[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] #[non_exhaustive] pub struct Config { /// Configuration for the 32-bit value stack (i32, f32, ref values). @@ -118,11 +117,11 @@ pub struct Config { pub call_stack: StackConfig, /// Fuel accounting policy used by budgeted execution. Defaults to [`FuelPolicy::PerInstruction`]. pub fuel_policy: FuelPolicy, - /// Backend used for runtime memories. Defaults to [`MemoryBackend::vec`]. - pub memory_backend: MemoryBackend, /// Whether memory and stack allocation failures should trap instead of degrading into normal operation failure modes. /// Defaults to `false`. pub trap_on_oom: bool, + /// Resource limiter shared across all stores created from this engine. Defaults to `None`. + pub resource_limiter: Option>, /// Initial number of GC heap bytes that triggers collection. /// Defaults to 1 MiB. pub gc_collection_threshold: usize, @@ -140,12 +139,6 @@ impl Config { self } - /// Set the backend used for runtime memories. - pub fn with_memory_backend(mut self, memory_backend: MemoryBackend) -> Self { - self.memory_backend = memory_backend; - self - } - /// Set the configuration used for the 32-bit value stack. pub fn with_value_stack_32(mut self, stack: StackConfig) -> Self { self.value_stack_32 = stack; @@ -184,6 +177,12 @@ impl Config { self } + /// Set the resource limiter shared across all stores created from this engine. + pub fn with_resource_limiter(mut self, limiter: Arc) -> Self { + self.resource_limiter = Some(limiter); + self + } + /// Set the initial GC heap collection threshold in bytes. pub fn with_gc_collection_threshold(mut self, threshold: usize) -> Self { self.gc_collection_threshold = threshold; @@ -195,11 +194,6 @@ impl Config { self.fuel_policy } - /// Get the current memory backend - pub fn memory_backend(&self) -> &MemoryBackend { - &self.memory_backend - } - pub(crate) const fn trap_on_oom(&self) -> bool { self.trap_on_oom } @@ -213,9 +207,25 @@ impl Default for Config { value_stack_128: StackConfig::fixed(DEFAULT_VALUE_STACK_128_SIZE), call_stack: StackConfig::fixed(DEFAULT_MAX_CALL_STACK_SIZE), fuel_policy: FuelPolicy::default(), - memory_backend: MemoryBackend::default(), trap_on_oom: false, + resource_limiter: None, gc_collection_threshold: 1024 * 1024, } } } + +#[cfg(feature = "debug")] +impl core::fmt::Debug for Config { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Config") + .field("value_stack_32", &self.value_stack_32) + .field("value_stack_64", &self.value_stack_64) + .field("value_stack_128", &self.value_stack_128) + .field("call_stack", &self.call_stack) + .field("fuel_policy", &self.fuel_policy) + .field("trap_on_oom", &self.trap_on_oom) + .field("resource_limiter", &self.resource_limiter.is_some()) + .field("gc_collection_threshold", &self.gc_collection_threshold) + .finish() + } +} diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index f45b7184..13b76abe 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -85,6 +85,17 @@ impl ModuleInstance { *self.0.mem_addrs.get(addr as usize).unwrap_or_else(|| unreachable!("invalid memory address: {addr}")) } + /// The resolved store address of the module's first memory, or `MemAddr::MAX` when the module + /// has none. + /// + /// The sentinel is never used in practice: validation guarantees a memory instruction can only + /// reference index 0 when the module declares a memory, so any read of the sentinel would trip + /// an out-of-bounds panic in the store lookup and surface a bug. + #[inline] + pub(crate) fn mem0_addr(&self) -> MemAddr { + self.0.mem_addrs.first().copied().unwrap_or(MemAddr::MAX) + } + /// resolve a data address to the global store address #[inline] pub(crate) fn resolve_data_addr(&self, addr: DataAddr) -> DataAddr { @@ -165,18 +176,7 @@ impl ModuleInstance { let imported_funcs = addrs.funcs.len(); addrs.funcs.extend(store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs)); addrs.tags.extend(store.init_tags(&module.tags, &type_addrs)); - match module.local_memory_allocation { - LocalMemoryAllocation::Skip => { - #[cfg(feature = "guest-debug")] - addrs.memories.extend(store.init_memories(&module.memory_types, MemoryInstance::new_lazy)?); - } - LocalMemoryAllocation::Lazy => { - addrs.memories.extend(store.init_memories(&module.memory_types, MemoryInstance::new_lazy)?) - } - LocalMemoryAllocation::Eager => { - addrs.memories.extend(store.init_memories(&module.memory_types, MemoryInstance::new)?) - } - } + addrs.memories.extend(store.init_memories(&module.memory_types, MemoryInstance::new)?); store.init_globals(&mut addrs.globals, &module.globals, &addrs.funcs, &type_addrs)?; addrs.tables.extend(store.init_tables(&module.tables, &addrs.globals, &addrs.funcs, &type_addrs)?); diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index 9da4a7ad..bacbe600 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -27,6 +27,7 @@ pub(crate) struct Executor<'store, const BUDGETED: bool> { module: ModuleInstance, store: &'store mut Store, call_stack_base: u32, + mem0: MemAddr, } impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { @@ -36,7 +37,22 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { .get_module_instance(wasm_func.owner) .unwrap_or_else(|| unreachable!("invalid module instance")) .clone(); - Self { module, cf, func: wasm_func.func.clone(), store, call_stack_base } + let mem0 = module.mem0_addr(); + Self { module, cf, func: wasm_func.func.clone(), store, call_stack_base, mem0 } + } + + /// Resolves a module-local memory index to its store address, caching the common memory-0 case. + #[inline(always)] + fn mem_addr(&self, idx: MemAddr) -> MemAddr { + if idx == 0 { self.mem0 } else { self.module.resolve_mem_addr(idx) } + } + + /// Switches the executor to another module, keeping the cached memory-0 address in sync. + #[inline] + fn set_module(&mut self, owner: ModuleInstanceId) { + self.module = + self.store.get_module_instance(owner).unwrap_or_else(|| unreachable!("invalid module instance")).clone(); + self.mem0 = self.module.mem0_addr(); } #[inline(always)] @@ -975,11 +991,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let wasm_func = self.store.state.get_wasm_func(self.cf.func_addr); self.func = wasm_func.func.clone(); if wasm_func.owner != self.module.id() { - self.module = self - .store - .get_module_instance(wasm_func.owner) - .unwrap_or_else(|| unreachable!("invalid module instance")) - .clone(); + self.set_module(wasm_func.owner); } } @@ -1038,11 +1050,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } self.cf = CallFrame::new(func_addr, locals_base, locals); if owner != self.module.id() { - self.module = self - .store - .get_module_instance(owner) - .unwrap_or_else(|| unreachable!("invalid module instance")) - .clone(); + self.set_module(owner); } Ok(()) @@ -1064,11 +1072,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } self.cf = CallFrame::new(func_addr, locals_base, locals); if owner != self.module.id() { - self.module = self - .store - .get_module_instance(owner) - .unwrap_or_else(|| unreachable!("invalid module instance")) - .clone(); + self.set_module(owner); } Ok(()) @@ -1277,7 +1281,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { value_local: u8, ) -> Result<(), Trap> { let value = T::local_get(&self.store.value_stack, &self.cf, u16::from(value_local)); - let mem_addr = self.module.resolve_mem_addr(memarg.mem_addr()); + let mem_addr = self.mem_addr(memarg.mem_addr()); let mem = self.store.state.get_mem(mem_addr); let addr = if mem.is_64bit() { let base = u64::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); @@ -1292,7 +1296,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { mem.effective_addr::(base as usize, memarg.offset())? }; let mem = self.store.state.get_mem_mut(mem_addr); - value.store_at(&mut *mem.inner, addr) + value.store_at(&mut mem.inner, addr) } #[inline(always)] @@ -1302,7 +1306,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { addr_local: u8, increment: impl FnOnce(T) -> T, ) -> Result<(), Trap> { - let mem_addr = self.module.resolve_mem_addr(memarg.mem_addr()); + let mem_addr = self.mem_addr(memarg.mem_addr()); let mem = self.store.state.get_mem(mem_addr); let addr = if mem.is_64bit() { let base = i64::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)) as u64; @@ -1318,8 +1322,8 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { }; let mem = self.store.state.get_mem_mut(mem_addr); - let value = cold_err!(T::load_at(&*mem.inner, addr))?; - increment(value).store_at(&mut *mem.inner, addr) + let value = cold_err!(T::load_at(&mem.inner, addr))?; + increment(value).store_at(&mut mem.inner, addr) } #[inline(always)] @@ -1334,12 +1338,12 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let lhs = T::stack_pop(&mut self.store.value_stack); let acc = T::stack_pop(&mut self.store.value_stack); let fma = acc + lhs * rhs; - let mem_addr = self.module.resolve_mem_addr(m.mem_addr()); + let mem_addr = self.mem_addr(m.mem_addr()); let mem = self.store.state.get_mem(mem_addr); let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; let addr = mem.effective_addr::(base, m.offset())?; let mem = self.store.state.get_mem_mut(mem_addr); - fma.store_at(&mut *mem.inner, addr)?; + fma.store_at(&mut mem.inner, addr)?; Ok(()) } @@ -1349,7 +1353,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { memarg: CompactMemoryArg, addr_local: u8, ) -> Result { - let mem = self.store.state.get_mem(self.module.resolve_mem_addr(memarg.mem_addr())); + let mem = self.store.state.get_mem(self.mem_addr(memarg.mem_addr())); let addr = if mem.is_64bit() { let base = i64::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)) as u64; let base = cold_err!(usize::try_from(base).map_err(|_| Trap::MemoryOutOfBounds { @@ -1362,7 +1366,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let base = u32::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); mem.effective_addr::(base as usize, memarg.offset())? }; - cold_err!(T::load_at(&*mem.inner, addr)) + cold_err!(T::load_at(&mem.inner, addr)) } #[inline(always)] @@ -1638,7 +1642,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } fn exec_memory_size(&mut self, addr: u32) -> Result<(), Trap> { - let mem = self.store.state.get_mem(self.module.resolve_mem_addr(addr)); + let mem = self.store.state.get_mem(self.mem_addr(addr)); match mem.is_64bit() { true => self.store.value_stack.push::(mem.page_count as i64), false => self.store.value_stack.push::(mem.page_count as i32), @@ -1646,14 +1650,17 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } fn exec_memory_grow(&mut self, addr: u32) -> Result<(), Trap> { - let mem = self.store.state.get_mem_mut(self.module.resolve_mem_addr(addr)); - let is_64bit = mem.is_64bit(); + let mem_addr = self.mem_addr(addr); + let is_64bit = self.store.state.get_mem(mem_addr).is_64bit(); let pages_delta = match is_64bit { true => ::stack_pop(&mut self.store.value_stack), false => i64::from(::stack_pop(&mut self.store.value_stack)), }; + let trap_on_oom = self.store.engine.config().trap_on_oom(); + let limiter = self.store.engine.config().resource_limiter.as_deref(); - let size = mem.grow(pages_delta, self.store.engine.config().trap_on_oom())?.unwrap_or(-1); + let mem = self.store.state.get_mem_mut(mem_addr); + let size = mem.grow(pages_delta, trap_on_oom, limiter)?.unwrap_or(-1); match is_64bit { true => self.store.value_stack.push::(size)?, false => self.store.value_stack.push::(size as i32)?, @@ -1664,8 +1671,8 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { fn exec_memory_copy(&mut self, index: OperandIdx) -> Result<(), Trap> { let TwoU32 { first: dst_mem, second: src_mem } = index.get(&self.func.data); - let dst_mem_addr = self.module.resolve_mem_addr(dst_mem); - let src_mem_addr = self.module.resolve_mem_addr(src_mem); + let dst_mem_addr = self.mem_addr(dst_mem); + let src_mem_addr = self.mem_addr(src_mem); let dst_arch = self.store.state.get_mem(dst_mem_addr).kind.arch(); let src_arch = self.store.state.get_mem(src_mem_addr).kind.arch(); let len_arch = @@ -1687,7 +1694,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } fn exec_memory_fill(&mut self, addr: u32) -> Result<(), Trap> { - let mem_addr = self.module.resolve_mem_addr(addr); + let mem_addr = self.mem_addr(addr); let arch = self.store.state.get_mem(mem_addr).kind.arch(); let size = self.store.value_stack.pop_memory_operand(arch)?; let val = i32::stack_pop(&mut self.store.value_stack); @@ -1697,7 +1704,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { fn exec_memory_fill_const(&mut self, index: OperandIdx) -> Result<(), Trap> { let MemoryFillConstOp { memory: addr, byte: val, value: size } = index.get(&self.func.data); - let mem_addr = self.module.resolve_mem_addr(addr); + let mem_addr = self.mem_addr(addr); let arch = self.store.state.get_mem(mem_addr).kind.arch(); let dst = self.store.value_stack.pop_memory_operand(arch)?; self.exec_memory_fill_impl(mem_addr, dst, val, size as u32 as usize) @@ -1706,7 +1713,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { fn exec_memory_fill_impl(&mut self, mem_addr: MemAddr, dst: usize, val: u8, size: usize) -> Result<(), Trap> { let mem = self.store.state.get_mem_mut(mem_addr); let max = mem.inner.len(); - if mem.inner.fill(dst, size, val)?.is_none() { + if mem.inner.fill(dst, size, val).is_none() { return cold!(Err(Trap::MemoryOutOfBounds { offset: dst, len: size, max })); } Ok(()) @@ -1716,7 +1723,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let TwoU32 { first: data_index, second: mem_index } = index.get(&self.func.data); let size = u32::stack_pop(&mut self.store.value_stack) as usize; let offset = u32::stack_pop(&mut self.store.value_stack) as usize; - let mem_addr = self.module.resolve_mem_addr(mem_index); + let mem_addr = self.mem_addr(mem_index); let arch = self.store.state.get_mem(mem_addr).kind.arch(); let dst = self.store.value_stack.pop_memory_operand(arch)?; @@ -1739,7 +1746,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { return cold!(Err(Trap::MemoryOutOfBounds { offset: 0, len: 0, max: 0 })); }; - if mem.inner.write_all(dst, &data[offset..offset + size])?.is_none() { + if mem.inner.write_all(dst, &data[offset..offset + size]).is_none() { return cold!(Err(Trap::MemoryOutOfBounds { offset: dst, len: size, max: mem_len })); } Ok(()) @@ -1772,10 +1779,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { arg: MemoryLaneArg, ) -> Result<(), Trap> { let m = arg.memory_arg_idx.get(&self.func.data); - let mem = self.store.state.get_mem(self.module.resolve_mem_addr(m.mem_addr())); + let mem = self.store.state.get_mem(self.mem_addr(m.mem_addr())); let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; let addr = mem.effective_addr::(base, m.offset())?; - let val = cold_err!(LOAD::load_at(&*mem.inner, addr))?; + let val = cold_err!(LOAD::load_at(&mem.inner, addr))?; let offset = arg.lane as usize * LOAD_SIZE; let mut imm = ::stack_pop(&mut self.store.value_stack).to_mem_bytes(); imm[offset..offset + LOAD_SIZE].copy_from_slice(&val.to_mem_bytes()); @@ -1790,10 +1797,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { cast: impl Fn(LOAD) -> TARGET, ) -> Result<(), Trap> { let m = index.get(&self.func.data); - let mem = self.store.state.get_mem(self.module.resolve_mem_addr(m.mem_addr())); + let mem = self.store.state.get_mem(self.mem_addr(m.mem_addr())); let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; let addr = mem.effective_addr::(base, m.offset())?; - let value = cold_err!(LOAD::load_at(&*mem.inner, addr))?; + let value = cold_err!(LOAD::load_at(&mem.inner, addr))?; self.store.value_stack.push(cast(value)) } @@ -1804,12 +1811,12 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { val_bytes.copy_from_slice(&bytes[lane_offset..lane_offset + N]); let val = U::from_mem_bytes(val_bytes); let m = arg.memory_arg_idx.get(&self.func.data); - let mem_addr = self.module.resolve_mem_addr(m.mem_addr()); + let mem_addr = self.mem_addr(m.mem_addr()); let mem = self.store.state.get_mem(mem_addr); let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; let addr = mem.effective_addr::(base, m.offset())?; let mem = self.store.state.get_mem_mut(mem_addr); - cold_err!(val.store_at(&mut *mem.inner, addr))?; + cold_err!(val.store_at(&mut mem.inner, addr))?; Ok(()) } @@ -1822,12 +1829,12 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let val = cast(val); let m = index.get(&self.func.data); - let mem_addr = self.module.resolve_mem_addr(m.mem_addr()); + let mem_addr = self.mem_addr(m.mem_addr()); let mem = self.store.state.get_mem(mem_addr); let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; let addr = mem.effective_addr::(base, m.offset())?; let mem = self.store.state.get_mem_mut(mem_addr); - cold_err!(val.store_at(&mut *mem.inner, addr))?; + cold_err!(val.store_at(&mut mem.inner, addr))?; Ok(()) } diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index e941430b..16fdca3b 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -54,8 +54,8 @@ //! ``` //! //! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`] -//! and [`engine::Config`] to control stack sizing, fuel accounting, memory backends, -//! and trap-on-OOM behavior. +//! and [`engine::Config`] to control stack sizing, fuel accounting, and trap-on-OOM +//! behavior. A [`ResourceLimiter`] can be attached to a [`Store`] to bound memory growth. //! //! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory. //! @@ -129,7 +129,7 @@ use interpreter::InterpreterRuntime; /// Global configuration for the WebAssembly interpreter pub mod engine; -pub use engine::{Engine, LazyLinearMemory, LinearMemory, MemoryBackend, PagedMemory, StackConfig, VecMemory}; +pub use engine::{Engine, StackConfig}; #[cfg(feature = "parser")] /// Re-export of [`tinywasm_parser`]. Requires `parser` feature. diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index f5d9c23a..4a11622c 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -122,7 +122,7 @@ impl crate::std::io::Read for MemoryCursor<'_> { impl crate::std::io::Write for MemoryCursor<'_> { fn write(&mut self, buf: &[u8]) -> crate::std::io::Result { let offset = self.offset()?; - let written = self.memory.inner.write(offset, buf).map_err(Error::from)?; + let written = self.memory.inner.write(offset, buf); self.advance(written)?; Ok(written) } @@ -163,7 +163,7 @@ impl Memory { /// Create a new memory in the given store. pub fn new(store: &mut Store, ty: MemoryType) -> Result { let addr = store.state.memories.len() as MemAddr; - store.state.memories.push(MemoryInstance::new(ty, &store.engine.config().memory_backend)?); + store.state.memories.push(MemoryInstance::new(ty)?); Ok(Self(StoreItem::new(store.id(), addr))) } @@ -242,7 +242,7 @@ impl Memory { /// Depending on the configured backend, this may return fewer bytes than requested even when /// more space is available. Use [`Self::copy_from_slice`] when you need the full slice written. pub fn write(&self, store: &mut Store, offset: usize, src: &[u8]) -> Result { - Ok(self.instance_mut(store)?.inner.write(offset, src)?) + Ok(self.instance_mut(store)?.inner.write(offset, src)) } /// Reads exactly `dst.len()` bytes from memory. @@ -265,7 +265,9 @@ impl Memory { /// Grow the memory by the given number of pages. pub fn grow(&self, store: &mut Store, delta_pages: i64) -> Result> { - self.instance_mut(store)?.grow(delta_pages, true).map_err(Into::into) + let limiter = store.engine.config().resource_limiter.clone(); + let mem = self.instance_mut(store)?; + mem.grow(delta_pages, true, limiter.as_deref()).map_err(Into::into) } /// Get the current size of the memory in pages. @@ -281,14 +283,14 @@ impl Memory { /// Fill a slice of memory with a value. pub fn fill(&self, store: &mut Store, offset: usize, len: usize, val: u8) -> Result<()> { - self.instance_mut(store)?.inner.fill(offset, len, val)?.ok_or_else(|| { + self.instance_mut(store)?.inner.fill(offset, len, val).ok_or_else(|| { Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len, max: self.instance(store).unwrap().inner.len() }) }) } /// Copies a full slice into memory. pub fn copy_from_slice(&self, store: &mut Store, offset: usize, data: &[u8]) -> Result<()> { - self.instance_mut(store)?.inner.write_all(offset, data)?.ok_or_else(|| { + self.instance_mut(store)?.inner.write_all(offset, data).ok_or_else(|| { Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len: data.len(), diff --git a/crates/tinywasm/src/store/memory/instance.rs b/crates/tinywasm/src/store/memory/instance.rs index 68a075b0..3fc4c5d7 100644 --- a/crates/tinywasm/src/store/memory/instance.rs +++ b/crates/tinywasm/src/store/memory/instance.rs @@ -1,7 +1,6 @@ -use alloc::format; use tinywasm_types::{MemoryArch, MemoryType}; -use crate::{Error, MemoryBackend, Result, Trap}; +use crate::{Error, ResourceLimiter, Result, Trap}; use super::{MemoryStorage, memory_oob}; use core::hint::cold_path; @@ -67,7 +66,7 @@ impl MemoryInstance { } } - pub(crate) fn new(kind: MemoryType, backend: &MemoryBackend) -> Result { + pub(crate) fn new(kind: MemoryType) -> Result { let initial_len = Self::host_size(kind, kind.page_count_initial())?; crate::log::debug!( @@ -76,27 +75,7 @@ impl MemoryInstance { kind.page_size() ); - let storage = backend.create(kind, initial_len)?; - if storage.len() != initial_len { - return Err(Error::Other(format!( - "memory backend returned {} bytes for a memory that requires {initial_len}", - storage.len() - ))); - } - - Ok(Self { kind, inner: storage, page_count: kind.page_count_initial() as usize }) - } - - pub(crate) fn new_lazy(kind: MemoryType, backend: &MemoryBackend) -> Result { - let initial_len = Self::host_size(kind, kind.page_count_initial())?; - - crate::log::debug!( - "initializing lazy memory with {} pages of {} bytes", - kind.page_count_initial(), - kind.page_size() - ); - - let storage = backend.create_lazy(kind, initial_len)?; + let storage = MemoryStorage::try_new(initial_len)?; Ok(Self { kind, inner: storage, page_count: kind.page_count_initial() as usize }) } @@ -137,7 +116,7 @@ impl MemoryInstance { cold_path(); memory_oob(src + copied, chunk_len, src_memory.inner.len()) })?; - self.inner.write_all(dst + copied, &buf[..chunk_len])?.ok_or_else(|| { + self.inner.write_all(dst + copied, &buf[..chunk_len]).ok_or_else(|| { cold_path(); memory_oob(dst + copied, chunk_len, self.inner.len()) })?; @@ -148,13 +127,18 @@ impl MemoryInstance { } pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result<(), Trap> { - self.inner.copy_within(dst, src, len)?.ok_or_else(|| { + self.inner.copy_within(dst, src, len).ok_or_else(|| { cold_path(); memory_oob(dst, len, self.inner.len()) }) } - pub(crate) fn grow(&mut self, pages_delta: i64, trap_on_oom: bool) -> Result, Trap> { + pub(crate) fn grow( + &mut self, + pages_delta: i64, + trap_on_oom: bool, + limiter: Option<&dyn ResourceLimiter>, + ) -> Result, Trap> { if pages_delta < 0 { cold_path(); crate::log::debug!("memory.grow failed: negative delta {}", pages_delta); @@ -183,6 +167,21 @@ impl MemoryInstance { return Ok(i64::try_from(current_pages).ok()); } + if let Some(limiter) = limiter { + let maximum = self.kind.page_count_max_declared().and_then(|pages| Self::host_size(self.kind, pages).ok()); + match limiter.memory_growing(self.inner.len(), new_size, maximum) { + Ok(true) => {} + Ok(false) => { + cold_path(); + return Ok(None); + } + Err(trap) => { + cold_path(); + return Err(trap); + } + } + } + if let Err(err) = self.inner.grow_to(new_size) { if trap_on_oom { return Err(err); diff --git a/crates/tinywasm/src/store/memory/lazy.rs b/crates/tinywasm/src/store/memory/lazy.rs deleted file mode 100644 index 780d5b5b..00000000 --- a/crates/tinywasm/src/store/memory/lazy.rs +++ /dev/null @@ -1,114 +0,0 @@ -use alloc::boxed::Box; - -use tinywasm_types::MemoryType; - -use crate::{Error, MemoryBackend, Result}; - -use super::LinearMemory; - -/// A linear memory wrapper that allocates its backend on the first mutation. -/// -/// Before materialization, the memory is represented by its logical length and -/// reads return the zeroes required by WebAssembly semantics. -pub struct LazyLinearMemory { - ty: MemoryType, - initial_len: usize, - backend: MemoryBackend, - inner: Option>, -} - -impl LazyLinearMemory { - /// Creates a lazy memory for `ty` using `backend` for eventual storage. - pub fn try_new(ty: MemoryType, backend: MemoryBackend) -> Result { - let page_size = usize::try_from(ty.page_size()) - .map_err(|_| Error::UnsupportedFeature("memory page size exceeds the host address space"))?; - let pages = usize::try_from(ty.page_count_initial()) - .map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?; - let initial_len = pages - .checked_mul(page_size) - .ok_or(Error::UnsupportedFeature("memory size exceeds the host address space"))?; - Ok(Self::new_with_initial_len(ty, initial_len, backend)) - } - - pub(crate) fn new_with_initial_len(ty: MemoryType, initial_len: usize, backend: MemoryBackend) -> Self { - Self { ty, initial_len, backend, inner: None } - } - - fn materialize(&mut self) -> core::result::Result<&mut dyn LinearMemory, crate::Trap> { - if self.inner.is_none() { - let storage = cold_err!(self.backend.create(self.ty, self.initial_len))?; - self.inner = Some(storage); - } - Ok(self.inner.as_deref_mut().expect("lazy memory should be materialized")) - } -} - -impl LinearMemory for LazyLinearMemory { - fn len(&self) -> usize { - self.inner.as_deref().map_or(self.initial_len, LinearMemory::len) - } - - fn grow_to(&mut self, new_len: usize) -> Result<(), crate::Trap> { - self.materialize()?.grow_to(new_len) - } - - fn read(&self, addr: usize, dst: &mut [u8]) -> usize { - if let Some(inner) = self.inner.as_deref() { - return inner.read(addr, dst); - } - if addr >= self.initial_len { - return 0; - } - let read_len = dst.len().min(self.initial_len - addr); - dst[..read_len].fill(0); - read_len - } - - fn write(&mut self, addr: usize, src: &[u8]) -> core::result::Result { - if src.is_empty() || addr >= self.len() { - return Ok(0); - } - self.materialize()?.write(addr, src) - } - - fn write_all(&mut self, addr: usize, src: &[u8]) -> core::result::Result, crate::Trap> { - let Some(end) = addr.checked_add(src.len()) else { return Ok(None) }; - if end > self.len() { - return Ok(None); - } - if src.is_empty() { - return Ok(Some(())); - } - self.materialize()?.write_all(addr, src) - } - - fn fill(&mut self, addr: usize, len: usize, val: u8) -> core::result::Result, crate::Trap> { - let Some(end) = addr.checked_add(len) else { return Ok(None) }; - if end > self.len() { - return Ok(None); - } - if len == 0 || val == 0 && self.inner.is_none() { - return Ok(Some(())); - } - self.materialize()?.fill(addr, len, val) - } - - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> core::result::Result, crate::Trap> { - let Some(src_end) = src.checked_add(len) else { return Ok(None) }; - let Some(dst_end) = dst.checked_add(len) else { return Ok(None) }; - if src_end > self.len() || dst_end > self.len() { - return Ok(None); - } - if self.inner.is_none() || len == 0 || dst == src { - return Ok(Some(())); - } - self.materialize()?.copy_within(dst, src, len) - } -} - -#[cfg(feature = "debug")] -impl core::fmt::Debug for LazyLinearMemory { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("LazyLinearMemory").field("ty", &self.ty).field("materialized", &self.inner.is_some()).finish() - } -} diff --git a/crates/tinywasm/src/store/memory/mod.rs b/crates/tinywasm/src/store/memory/mod.rs index 70ffb209..24f295ba 100644 --- a/crates/tinywasm/src/store/memory/mod.rs +++ b/crates/tinywasm/src/store/memory/mod.rs @@ -1,372 +1,32 @@ -use alloc::{boxed::Box, sync::Arc}; -use alloc::{vec, vec::Vec}; -use core::cmp::min; -use core::hint::cold_path; - -use tinywasm_types::MemoryType; - -use crate::Result; use crate::interpreter::Value128; mod instance; -mod lazy; - -mod paged; -#[path = "vec.rs"] -mod vec_memory; +mod vec; pub(crate) use instance::MemoryInstance; -pub use {lazy::LazyLinearMemory, paged::PagedMemory, vec_memory::VecMemory}; +pub(crate) use vec::VecMemory; -/// Backend storage for a linear memory +/// Internal storage for a linear memory. /// -/// This is a low-level trait that abstracts over the actual storage mechanism for linear memory. -/// This will probably change in the future to allow more efficient implementations. -/// See [`MemoryBackend`] for a higher-level interface to configuring memory storage. -/// The runtime passes slices of the exact indicated width to the fixed-width `write_*` methods. -pub trait LinearMemory { - /// Returns the current memory length in bytes. - fn len(&self) -> usize; - - /// Returns true if the memory is empty. - fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Grows the memory to `new_len` bytes. - /// - /// The runtime only calls this with lengths that are exact multiples of the Wasm page size for - /// the owning memory. - fn grow_to(&mut self, new_len: usize) -> core::result::Result<(), crate::Trap>; - - /// Reads up to `dst.len()` bytes starting at `addr` and returns the number of bytes read. - /// - /// Backends may return fewer bytes than requested even when more data is available. This lets - /// non-contiguous backends stop at a natural boundary such as the end of a chunk. - fn read(&self, addr: usize, dst: &mut [u8]) -> usize; - - /// Writes up to `src.len()` bytes starting at `addr` and returns the number of bytes written. - /// - /// Backends may return fewer bytes than requested even when more space is available. This lets - /// non-contiguous backends stop at a natural boundary such as the end of a chunk. Backend - /// failures are returned as traps. - fn write(&mut self, addr: usize, src: &[u8]) -> core::result::Result; - - /// Writes all bytes in `src`, returns `Ok(None)` for an invalid range, or returns a backend trap. - fn write_all(&mut self, addr: usize, src: &[u8]) -> core::result::Result, crate::Trap> { - let Some(end) = addr.checked_add(src.len()) else { - return cold!(Ok(None)); - }; - - if end > self.len() { - return cold!(Ok(None)); - } - - let mut offset = 0; - while offset < src.len() { - let written = self.write(addr + offset, &src[offset..])?; - if written == 0 { - return cold!(Ok(None)); - } - offset += written; - } - - Ok(Some(())) - } - - /// Fills the range `[addr, addr + len)` with `val`. - fn fill(&mut self, addr: usize, len: usize, val: u8) -> core::result::Result, crate::Trap> { - let Some(end) = addr.checked_add(len) else { return Ok(None) }; - if end > self.len() { - return Ok(None); - } - - let chunk = [val; 1024]; - let mut offset = 0; - while offset < len { - let chunk_len = min(len - offset, 1024); - if self.write_all(addr + offset, &chunk[..chunk_len])?.is_none() { - return Ok(None); - } - offset += chunk_len; - } - - Ok(Some(())) - } - - /// Copies `len` bytes from `src` to `dst` within the same memory. - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> core::result::Result, crate::Trap> { - let Some(src_end) = src.checked_add(len) else { return Ok(None) }; - let Some(dst_end) = dst.checked_add(len) else { return Ok(None) }; - if src_end > self.len() || dst_end > self.len() { - return Ok(None); - } - - if len == 0 || dst == src { - return Ok(Some(())); - } - - let mut chunk = [0; 1024]; - - // If the source and destination ranges are disjoint, we can copy forward without a temporary buffer. - if dst < src || dst >= src_end { - let mut offset = 0; - while offset < len { - let chunk_len = min(len - offset, 1024); - if self.read_exact(src + offset, &mut chunk[..chunk_len]).is_none() - || self.write_all(dst + offset, &chunk[..chunk_len])?.is_none() - { - return Ok(None); - } - offset += chunk_len; - } - } else { - // Otherwise, we need to copy backward to avoid overwriting the source data before it's read. - let mut offset = len; - while offset > 0 { - let chunk_len = min(offset, 1024); - offset -= chunk_len; - if self.read_exact(src + offset, &mut chunk[..chunk_len]).is_none() - || self.write_all(dst + offset, &chunk[..chunk_len])?.is_none() - { - return Ok(None); - } - } - } - - Ok(Some(())) - } - - /// Reads exactly `dst.len()` bytes starting at `addr`. - fn read_exact(&self, addr: usize, dst: &mut [u8]) -> Option<()> { - let Some(end) = addr.checked_add(dst.len()) else { - return cold!(None); - }; - - if end > self.len() { - return cold!(None); - } - - let mut offset = 0; - while offset < dst.len() { - let read = self.read(addr + offset, &mut dst[offset..]); - if read == 0 { - return cold!(None); - } - offset += read; - } - - Some(()) - } - - /// Reads `len` bytes starting at `addr` into a newly allocated buffer. - fn read_vec(&self, addr: usize, len: usize) -> Option> { - let end = addr.checked_add(len)?; - if end > self.len() { - return None; - } - - let mut data = vec![0; len]; - self.read_exact(addr, &mut data)?; - Some(data) - } - - /// Reads exactly 1 byte at `addr`. - fn read_8(&self, addr: usize) -> core::result::Result<[u8; 1], crate::Trap> { - let mut bytes = [0; 1]; - self.read_exact(addr, &mut bytes).ok_or_else(|| { - cold_path(); - memory_oob(addr, 1, self.len()) - })?; - Ok(bytes) - } - - /// Reads exactly 2 bytes at `addr`. - fn read_16(&self, addr: usize) -> core::result::Result<[u8; 2], crate::Trap> { - let mut bytes = [0; 2]; - self.read_exact(addr, &mut bytes).ok_or_else(|| { - cold_path(); - memory_oob(addr, 2, self.len()) - })?; - Ok(bytes) - } - - /// Reads exactly 4 bytes at `addr`. - fn read_32(&self, addr: usize) -> core::result::Result<[u8; 4], crate::Trap> { - let mut bytes = [0; 4]; - self.read_exact(addr, &mut bytes).ok_or_else(|| { - cold_path(); - memory_oob(addr, 4, self.len()) - })?; - Ok(bytes) - } - - /// Reads exactly 8 bytes at `addr`. - fn read_64(&self, addr: usize) -> core::result::Result<[u8; 8], crate::Trap> { - let mut bytes = [0; 8]; - self.read_exact(addr, &mut bytes).ok_or_else(|| { - cold_path(); - memory_oob(addr, 8, self.len()) - })?; - Ok(bytes) - } - - /// Reads exactly 16 bytes at `addr`. - fn read_128(&self, addr: usize) -> core::result::Result<[u8; 16], crate::Trap> { - let mut bytes = [0; 16]; - self.read_exact(addr, &mut bytes).ok_or_else(|| { - cold_path(); - memory_oob(addr, 16, self.len()) - })?; - Ok(bytes) - } - - /// Writes exactly 1 byte at `addr`. - fn write_8(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes)?.ok_or_else(|| { - cold_path(); - memory_oob(addr, 1, self.len()) - }) - } - - /// Writes exactly 2 bytes at `addr`. - fn write_16(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes)?.ok_or_else(|| { - cold_path(); - memory_oob(addr, 2, self.len()) - }) - } - - /// Writes exactly 4 bytes at `addr`. - fn write_32(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes)?.ok_or_else(|| { - cold_path(); - memory_oob(addr, 4, self.len()) - }) - } - - /// Writes exactly 8 bytes at `addr`. - fn write_64(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes)?.ok_or_else(|| { - cold_path(); - memory_oob(addr, 8, self.len()) - }) - } - - /// Writes exactly 16 bytes at `addr`. - fn write_128(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes)?.ok_or_else(|| { - cold_path(); - memory_oob(addr, 16, self.len()) - }) - } -} - -type MemoryFactory = dyn Fn(MemoryType) -> core::result::Result, crate::Trap> + Send + Sync; - -/// Configures how runtime memory instances are created. -#[derive(Clone, Default)] -pub struct MemoryBackend(MemoryBackendInner); - -#[derive(Clone, Default)] -enum MemoryBackendInner { - #[default] - Vec, - Paged { - chunk_size: usize, - }, - Custom(Arc), -} - -impl MemoryBackend { - /// Uses a contiguous [`VecMemory`] for each memory instance. - /// - /// This is usually the fastest option for reads and writes, but large grows can be expensive - /// because they may reallocate and copy the entire buffer. - pub const fn vec() -> Self { - Self(MemoryBackendInner::Vec) - } - - /// Uses sparse chunked storage for each memory instance. - /// - /// `chunk_size` is the backend chunk size in bytes. It must be a non-zero power - /// of two and is independent from the Wasm page size. - /// - /// This generally makes growth cheaper than [`Self::vec`], but read and write operations do a - /// little more work and may be slightly slower. - pub fn paged(chunk_size: usize) -> Self { - assert!(chunk_size.is_power_of_two(), "chunk_size must be a non-zero power of two"); - Self(MemoryBackendInner::Paged { chunk_size }) - } - - /// Uses a custom factory to create memory instances. - /// - /// Factory traps are returned during eager creation or when a lazy memory first materializes. - pub fn custom(factory: F) -> Self - where - F: Fn(MemoryType) -> core::result::Result + Send + Sync + 'static, - M: LinearMemory + 'static, - { - Self(MemoryBackendInner::Custom(Arc::new(move |ty| { - let memory = factory(ty)?; - Ok(Box::new(memory) as Box) - }))) - } - - pub(crate) fn create( - &self, - ty: MemoryType, - initial_len: usize, - ) -> core::result::Result { - let storage = match &self.0 { - MemoryBackendInner::Vec => Box::new(VecMemory::try_new(initial_len)?) as Box, - MemoryBackendInner::Paged { chunk_size } => { - Box::new(PagedMemory::try_new(initial_len, *chunk_size)?) as Box - } - MemoryBackendInner::Custom(factory) => factory(ty)?, - }; - - if storage.len() < initial_len { - return Err(crate::Trap::Other("memory backend returned less storage than required")); - } - - Ok(storage) - } - - pub(crate) fn create_lazy(&self, ty: MemoryType, initial_len: usize) -> Result { - Ok(Box::new(LazyLinearMemory::new_with_initial_len(ty, initial_len, self.clone()))) - } -} - -#[cfg(feature = "debug")] -impl core::fmt::Debug for MemoryBackend { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match &self.0 { - MemoryBackendInner::Vec => f.debug_tuple("MemoryBackend::Vec").finish(), - MemoryBackendInner::Paged { chunk_size } => { - f.debug_struct("MemoryBackend::Paged").field("chunk_size", chunk_size).finish() - } - MemoryBackendInner::Custom(_) => f.debug_tuple("MemoryBackend::Custom").finish(), - } - } -} - -pub(crate) type MemoryStorage = Box; +/// This is the boundary between the interpreter and the backing representation. Keeping it a +/// concrete type selected at compile time means the executor's load and store paths stay the same +/// whether the memory is `Vec`-backed or, later, mmap-backed. +pub(crate) type MemoryStorage = VecMemory; -/// A trait for types that can be converted to and from static byte arrays +/// A trait for types that can be converted to and from static byte arrays. pub(crate) trait MemValue: Copy + Default { - /// Store a value in memory + /// Store a value in memory. fn to_mem_bytes(self) -> [u8; N]; fn from_mem_bytes(bytes: [u8; N]) -> Self; - fn load_at(mem: &dyn LinearMemory, addr: usize) -> core::result::Result; + fn load_at(mem: &MemoryStorage, addr: usize) -> core::result::Result; - fn store_at(self, mem: &mut dyn LinearMemory, addr: usize) -> core::result::Result<(), crate::Trap>; + fn store_at(self, mem: &mut MemoryStorage, addr: usize) -> core::result::Result<(), crate::Trap>; } macro_rules! impl_mem_traits { - ($($ty:ty, $size:expr, $read:ident, $write:ident),* $(,)?) => { + ($($ty:ty, $size:expr),* $(,)?) => { $( impl MemValue<$size> for $ty { #[inline(always)] @@ -380,28 +40,20 @@ macro_rules! impl_mem_traits { } #[inline(always)] - fn load_at(mem: &dyn LinearMemory, addr: usize) -> core::result::Result { - Ok(Self::from_le_bytes(cold_err!(mem.$read(addr))?)) + fn load_at(mem: &MemoryStorage, addr: usize) -> core::result::Result { + Ok(Self::from_le_bytes(cold_err!(mem.read_fixed::<$size>(addr))?)) } #[inline(always)] - fn store_at( - self, - mem: &mut dyn LinearMemory, - addr: usize, - ) -> core::result::Result<(), crate::Trap> { - mem.$write(addr, &self.to_mem_bytes()) + fn store_at(self, mem: &mut MemoryStorage, addr: usize) -> core::result::Result<(), crate::Trap> { + mem.write_fixed::<$size>(addr, &self.to_mem_bytes()) } } )* }; } -impl_mem_traits!( - u8, 1, read_8, write_8, i8, 1, read_8, write_8, u16, 2, read_16, write_16, i16, 2, read_16, write_16, u32, 4, - read_32, write_32, i32, 4, read_32, write_32, f32, 4, read_32, write_32, u64, 8, read_64, write_64, i64, 8, - read_64, write_64, f64, 8, read_64, write_64 -); +impl_mem_traits!(u8, 1, i8, 1, u16, 2, i16, 2, u32, 4, i32, 4, f32, 4, u64, 8, i64, 8, f64, 8); impl MemValue<16> for Value128 { #[inline(always)] @@ -415,13 +67,13 @@ impl MemValue<16> for Value128 { } #[inline(always)] - fn load_at(mem: &dyn LinearMemory, addr: usize) -> core::result::Result { - Ok(Self(cold_err!(mem.read_128(addr))?)) + fn load_at(mem: &MemoryStorage, addr: usize) -> core::result::Result { + Ok(Self(cold_err!(mem.read_fixed::<16>(addr))?)) } #[inline(always)] - fn store_at(self, mem: &mut dyn LinearMemory, addr: usize) -> core::result::Result<(), crate::Trap> { - mem.write_128(addr, &self.0) + fn store_at(self, mem: &mut MemoryStorage, addr: usize) -> core::result::Result<(), crate::Trap> { + mem.write_fixed::<16>(addr, &self.0) } } diff --git a/crates/tinywasm/src/store/memory/paged.rs b/crates/tinywasm/src/store/memory/paged.rs deleted file mode 100644 index 3e6e5ea3..00000000 --- a/crates/tinywasm/src/store/memory/paged.rs +++ /dev/null @@ -1,368 +0,0 @@ -use alloc::boxed::Box; -use alloc::vec::Vec; -use core::cmp::min; -use core::hint::cold_path; - -use super::{LinearMemory, memory_oob}; - -/// A sparse chunked linear memory. -/// -/// This backend stores memory in fixed-size chunks, which makes growth cheaper because it avoids -/// resizing and copying one large contiguous buffer. -/// -/// The tradeoff is that reads and writes do a bit more bookkeeping and may need to cross chunk -/// boundaries, so they are usually slightly slower than [`super::VecMemory`]. -/// -/// In particular, [`LinearMemory::read`] and [`LinearMemory::write`] return at most the bytes up to -/// the end of the current chunk. Higher-level exact helpers loop over these short operations when -/// they need a full range. -pub struct PagedMemory { - len: usize, - chunk_size: usize, - chunk_shift: u32, - chunk_mask: usize, - chunks: Vec>>, -} - -impl PagedMemory { - /// Tries to create a new sparse memory with `len` addressable bytes and the given `chunk_size`. - /// - /// Prefer this backend when grow behavior matters more than absolute read and write speed. - pub fn try_new(len: usize, chunk_size: usize) -> Result { - assert!(chunk_size.is_power_of_two(), "chunk_size must be a power of two"); - - let mut memory = Self { - len: 0, - chunk_size, - chunk_shift: chunk_size.trailing_zeros(), - chunk_mask: chunk_size - 1, - chunks: Vec::new(), - }; - memory.grow_to(len)?; - Ok(memory) - } - - #[inline(always)] - fn allocate_chunk(&self) -> Result, crate::Trap> { - let mut chunk = Vec::new(); - cold_err!(chunk.try_reserve_exact(self.chunk_size)).map_err(|_| crate::Trap::OutOfMemory)?; - chunk.resize(self.chunk_size, 0); - Ok(chunk.into_boxed_slice()) - } - - #[inline(always)] - fn chunk_mut(&mut self, chunk_idx: usize) -> Result<&mut [u8], crate::Trap> { - if self.chunks[chunk_idx].is_none() { - self.chunks[chunk_idx] = Some(self.allocate_chunk()?); - } - - Ok(self.chunks[chunk_idx].as_deref_mut().unwrap_or_else(|| unreachable!())) - } - - #[inline(always)] - fn chunk_slice(&self, chunk_idx: usize) -> Option<&[u8]> { - self.chunks[chunk_idx].as_deref() - } - - #[inline(always)] - fn read_fixed(&self, addr: usize) -> Result<[u8; N], crate::Trap> { - let Some(end) = addr.checked_add(N).filter(|end| *end <= self.len) else { - return cold!(Err(memory_oob(addr, N, self.len))); - }; - let chunk_idx = addr >> self.chunk_shift; - let chunk_offset = addr & self.chunk_mask; - if end <= ((chunk_idx + 1) << self.chunk_shift) { - let mut bytes = [0; N]; - if let Some(chunk) = self.chunk_slice(chunk_idx) { - bytes.copy_from_slice(&chunk[chunk_offset..chunk_offset + N]); - } - return Ok(bytes); - } - cold_path(); - let mut bytes = [0; N]; - self.read_exact(addr, &mut bytes).ok_or_else(|| memory_oob(addr, N, self.len))?; - Ok(bytes) - } - - #[inline(always)] - fn write_fixed(&mut self, addr: usize, bytes: &[u8]) -> Result<(), crate::Trap> { - let Some(end) = addr.checked_add(N).filter(|end| *end <= self.len) else { - return cold!(Err(memory_oob(addr, N, self.len))); - }; - let chunk_idx = addr >> self.chunk_shift; - let chunk_offset = addr & self.chunk_mask; - if end <= ((chunk_idx + 1) << self.chunk_shift) { - self.chunk_mut(chunk_idx)?[chunk_offset..chunk_offset + N].copy_from_slice(bytes); - return Ok(()); - } - cold_path(); - self.write_all(addr, bytes)?.ok_or_else(|| memory_oob(addr, N, self.len)) - } - - #[inline(always)] - fn checked_end(&self, addr: usize, len: usize) -> Option { - let end = addr.checked_add(len)?; - if end > self.len { - return None; - } - Some(end) - } - - #[inline(always)] - fn copy_within_single_chunk(&mut self, dst: usize, src: usize, len: usize) -> bool { - if len == 0 { - return true; - } - - if self.checked_end(src, len).is_none() || self.checked_end(dst, len).is_none() { - return false; - } - - let src_chunk_idx = src >> self.chunk_shift; - let dst_chunk_idx = dst >> self.chunk_shift; - if src_chunk_idx != dst_chunk_idx { - return false; - } - - let src_offset = src & self.chunk_mask; - let dst_offset = dst & self.chunk_mask; - if src_offset + len > self.chunk_size || dst_offset + len > self.chunk_size { - return false; - } - - if let Some(Some(chunk)) = self.chunks.get_mut(src_chunk_idx) { - chunk.copy_within(src_offset..src_offset + len, dst_offset); - } - - true - } -} - -#[cfg(feature = "debug")] -impl core::fmt::Debug for PagedMemory { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let allocated_chunks = self.chunks.iter().filter(|chunk| chunk.is_some()).count(); - f.debug_struct("PagedMemory") - .field("len", &self.len) - .field("chunk_size", &self.chunk_size) - .field("allocated_chunks", &allocated_chunks) - .finish() - } -} - -impl LinearMemory for PagedMemory { - #[inline(always)] - fn len(&self) -> usize { - self.len - } - - #[inline(always)] - fn grow_to(&mut self, new_len: usize) -> Result<(), crate::Trap> { - if new_len < self.len { - return Err(crate::Trap::MemoryOutOfBounds { offset: new_len, len: 0, max: self.len }); - } - - let new_chunk_count = if new_len == 0 { 0 } else { new_len.div_ceil(self.chunk_size) }; - if new_chunk_count > self.chunks.len() { - cold_err!(self.chunks.try_reserve_exact(new_chunk_count - self.chunks.len())) - .map_err(|_| crate::Trap::OutOfMemory)?; - self.chunks.resize_with(new_chunk_count, || None); - } else { - self.chunks.truncate(new_chunk_count); - } - - self.len = new_len; - Ok(()) - } - - #[inline(always)] - fn read(&self, addr: usize, dst: &mut [u8]) -> usize { - if addr >= self.len || dst.is_empty() { - return 0; - } - - let chunk_idx = addr >> self.chunk_shift; - let chunk_offset = addr & self.chunk_mask; - let chunk_end = min((chunk_idx + 1) << self.chunk_shift, self.len); - let read_len = min(chunk_end - addr, dst.len()); - if let Some(chunk) = self.chunk_slice(chunk_idx) { - dst[..read_len].copy_from_slice(&chunk[chunk_offset..chunk_offset + read_len]); - } else { - dst[..read_len].fill(0); - } - - read_len - } - - #[inline(always)] - fn write(&mut self, addr: usize, src: &[u8]) -> Result { - if addr >= self.len || src.is_empty() { - return Ok(0); - } - - let chunk_idx = addr >> self.chunk_shift; - let chunk_offset = addr & self.chunk_mask; - let write_len = min(min(self.chunk_size - chunk_offset, self.len - addr), src.len()); - - let chunk = self.chunk_mut(chunk_idx)?; - chunk[chunk_offset..chunk_offset + write_len].copy_from_slice(&src[..write_len]); - Ok(write_len) - } - - #[inline(always)] - fn write_all(&mut self, addr: usize, src: &[u8]) -> Result, crate::Trap> { - let Some(end) = self.checked_end(addr, src.len()) else { return Ok(None) }; - let mut pos = addr; - let mut src_offset = 0; - - while pos < end { - let chunk_idx = pos >> self.chunk_shift; - let chunk_offset = pos & self.chunk_mask; - let copy_len = min(self.chunk_size - chunk_offset, end - pos); - - let chunk = self.chunk_mut(chunk_idx)?; - chunk[chunk_offset..chunk_offset + copy_len].copy_from_slice(&src[src_offset..src_offset + copy_len]); - - pos += copy_len; - src_offset += copy_len; - } - - Ok(Some(())) - } - - #[inline(always)] - fn fill(&mut self, addr: usize, len: usize, val: u8) -> Result, crate::Trap> { - let Some(end) = self.checked_end(addr, len) else { return Ok(None) }; - let mut pos = addr; - - while pos < end { - let chunk_idx = pos >> self.chunk_shift; - let chunk_offset = pos & self.chunk_mask; - let chunk_start = chunk_idx << self.chunk_shift; - let chunk_full_len = min(self.chunk_size, self.len - chunk_start); - let chunk_end = min(chunk_start + self.chunk_size, end); - let fill_len = chunk_end - pos; - - if val == 0 { - if chunk_offset == 0 && fill_len == chunk_full_len { - self.chunks[chunk_idx] = None; - } else if let Some(Some(chunk)) = self.chunks.get_mut(chunk_idx) { - chunk[chunk_offset..chunk_offset + fill_len].fill(0); - } - } else { - self.chunk_mut(chunk_idx)?[chunk_offset..chunk_offset + fill_len].fill(val); - } - - pos = chunk_end; - } - - Ok(Some(())) - } - - #[inline(always)] - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result, crate::Trap> { - if self.checked_end(src, len).is_none() || self.checked_end(dst, len).is_none() { - return Ok(None); - } - - if len == 0 || dst == src { - return Ok(Some(())); - } - - if self.copy_within_single_chunk(dst, src, len) { - return Ok(Some(())); - } - - let mut buf = [0u8; 256]; - - if dst < src || dst >= src + len { - let mut copied = 0; - while copied < len { - let chunk_len = min(buf.len(), len - copied); - if self.read_exact(src + copied, &mut buf[..chunk_len]).is_none() - || self.write_all(dst + copied, &buf[..chunk_len])?.is_none() - { - return Ok(None); - } - copied += chunk_len; - } - } else { - let mut remaining = len; - while remaining > 0 { - let chunk_len = min(buf.len(), remaining); - let chunk_start = remaining - chunk_len; - if self.read_exact(src + chunk_start, &mut buf[..chunk_len]).is_none() - || self.write_all(dst + chunk_start, &buf[..chunk_len])?.is_none() - { - return Ok(None); - } - remaining = chunk_start; - } - } - - Ok(Some(())) - } - - #[inline(always)] - fn read_8(&self, addr: usize) -> core::result::Result<[u8; 1], crate::Trap> { - if addr >= self.len { - cold_path(); - return Err(memory_oob(addr, 1, self.len)); - } - let chunk_idx = addr >> self.chunk_shift; - let chunk_offset = addr & self.chunk_mask; - Ok([self.chunk_slice(chunk_idx).map_or(0, |chunk| chunk[chunk_offset])]) - } - - #[inline(always)] - fn read_16(&self, addr: usize) -> core::result::Result<[u8; 2], crate::Trap> { - self.read_fixed::<2>(addr) - } - - #[inline(always)] - fn read_32(&self, addr: usize) -> core::result::Result<[u8; 4], crate::Trap> { - self.read_fixed::<4>(addr) - } - - #[inline(always)] - fn read_64(&self, addr: usize) -> core::result::Result<[u8; 8], crate::Trap> { - self.read_fixed::<8>(addr) - } - - #[inline(always)] - fn read_128(&self, addr: usize) -> core::result::Result<[u8; 16], crate::Trap> { - self.read_fixed::<16>(addr) - } - - #[inline(always)] - fn write_8(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - if addr >= self.len { - cold_path(); - return Err(memory_oob(addr, 1, self.len)); - } - let chunk_idx = addr >> self.chunk_shift; - let chunk_offset = addr & self.chunk_mask; - self.chunk_mut(chunk_idx)?[chunk_offset] = bytes[0]; - Ok(()) - } - - #[inline(always)] - fn write_16(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_fixed::<2>(addr, bytes) - } - - #[inline(always)] - fn write_32(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_fixed::<4>(addr, bytes) - } - - #[inline(always)] - fn write_64(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_fixed::<8>(addr, bytes) - } - - #[inline(always)] - fn write_128(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_fixed::<16>(addr, bytes) - } -} diff --git a/crates/tinywasm/src/store/memory/vec.rs b/crates/tinywasm/src/store/memory/vec.rs index be85e9b5..1b90e90e 100644 --- a/crates/tinywasm/src/store/memory/vec.rs +++ b/crates/tinywasm/src/store/memory/vec.rs @@ -1,24 +1,19 @@ use alloc::vec::Vec; -use super::{LinearMemory, memory_oob}; +use super::memory_oob; -/// A contiguous `Vec`-backed linear memory. +/// A contiguous `Vec`-backed linear memory storage. /// -/// This is the simplest backend and typically gives the best read and write throughput because -/// the whole memory lives in one contiguous allocation. -/// -/// The tradeoff is growth cost: large grows may need to reallocate and copy the full buffer, -/// which can get expensive for large memories. -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct VecMemory { +/// This is the internal storage boundary for [`super::MemoryInstance`]. Keeping it a concrete type +/// rather than a `Vec` directly means the backing representation can later be swapped for an +/// mmap-backed implementation without touching the interpreter's load and store paths. +pub(crate) struct VecMemory { data: Vec, } impl VecMemory { /// Tries to create a new memory with `len` zero-initialized bytes. - /// - /// Prefer this backend when contiguous access is more important than grow performance. - pub fn try_new(len: usize) -> Result { + pub(crate) fn try_new(len: usize) -> Result { let mut data = Vec::new(); cold_err!(data.try_reserve_exact(len)).map_err(|_| crate::Trap::OutOfMemory)?; data.resize(len, 0); @@ -26,11 +21,18 @@ impl VecMemory { } #[inline(always)] - fn read_fixed(&self, addr: usize) -> Result<[u8; N], crate::Trap> { - self.check_fixed_addr::(addr)?; - let mut bytes = [0u8; N]; - bytes.copy_from_slice(&self.data[addr..addr + N]); - Ok(bytes) + pub(crate) fn len(&self) -> usize { + self.data.len() + } + + /// Grows the backing allocation to `new_len`. Only called after the Wasm limits and any user + /// limiter have accepted the grow. + #[inline(always)] + pub(crate) fn grow_to(&mut self, new_len: usize) -> Result<(), crate::Trap> { + debug_assert!(new_len >= self.data.len(), "memory only grows"); + cold_err!(self.data.try_reserve_exact(new_len - self.data.len())).map_err(|_| crate::Trap::OutOfMemory)?; + self.data.resize(new_len, 0); + Ok(()) } #[inline(always)] @@ -40,27 +42,27 @@ impl VecMemory { } Ok(()) } -} -impl LinearMemory for VecMemory { + /// Reads exactly `N` bytes at `addr` into a fixed-size array. #[inline(always)] - fn len(&self) -> usize { - self.data.len() + pub(crate) fn read_fixed(&self, addr: usize) -> Result<[u8; N], crate::Trap> { + self.check_fixed_addr::(addr)?; + let mut bytes = [0u8; N]; + bytes.copy_from_slice(&self.data[addr..addr + N]); + Ok(bytes) } + /// Writes exactly `N` bytes from `bytes` at `addr`. #[inline(always)] - fn grow_to(&mut self, new_len: usize) -> Result<(), crate::Trap> { - if new_len < self.data.len() { - return Err(crate::Trap::MemoryOutOfBounds { offset: new_len, len: 0, max: self.data.len() }); - } - cold_err!(self.data.try_reserve_exact(new_len.saturating_sub(self.data.len()))) - .map_err(|_| crate::Trap::OutOfMemory)?; - self.data.resize(new_len, 0); + pub(crate) fn write_fixed(&mut self, addr: usize, bytes: &[u8]) -> Result<(), crate::Trap> { + self.check_fixed_addr::(addr)?; + self.data[addr..addr + N].copy_from_slice(bytes); Ok(()) } + /// Reads up to `dst.len()` bytes starting at `addr` and returns the number of bytes read. #[inline(always)] - fn read(&self, addr: usize, dst: &mut [u8]) -> usize { + pub(crate) fn read(&self, addr: usize, dst: &mut [u8]) -> usize { if addr >= self.data.len() { return 0; } @@ -69,114 +71,59 @@ impl LinearMemory for VecMemory { read_len } + /// Writes up to `src.len()` bytes starting at `addr` and returns the number of bytes written. #[inline(always)] - fn read_exact(&self, addr: usize, dst: &mut [u8]) -> Option<()> { - dst.copy_from_slice(self.data.get(addr..addr.checked_add(dst.len())?)?); - Some(()) + pub(crate) fn write(&mut self, addr: usize, src: &[u8]) -> usize { + if addr >= self.data.len() { + return 0; + } + let write_len = src.len().min(self.data.len() - addr); + self.data[addr..addr + write_len].copy_from_slice(&src[..write_len]); + write_len } + /// Reads exactly `dst.len()` bytes starting at `addr`, returning `None` for an invalid range. #[inline(always)] - fn read_vec(&self, addr: usize, len: usize) -> Option> { - Some(self.data.get(addr..addr.checked_add(len)?)?.to_vec()) + pub(crate) fn read_exact(&self, addr: usize, dst: &mut [u8]) -> Option<()> { + dst.copy_from_slice(self.data.get(addr..addr.checked_add(dst.len())?)?); + Some(()) } + /// Reads `len` bytes starting at `addr` into a newly allocated buffer, returning `None` for an + /// invalid range. #[inline(always)] - fn write(&mut self, addr: usize, src: &[u8]) -> Result { - if addr >= self.data.len() { - return Ok(0); - } - - let write_len = src.len().min(self.data.len() - addr); - self.data[addr..addr + write_len].copy_from_slice(&src[..write_len]); - Ok(write_len) + pub(crate) fn read_vec(&self, addr: usize, len: usize) -> Option> { + Some(self.data.get(addr..addr.checked_add(len)?)?.to_vec()) } + /// Writes all of `src` at `addr`, returning `None` for an invalid range. #[inline(always)] - fn write_all(&mut self, addr: usize, src: &[u8]) -> Result, crate::Trap> { - let Some(end) = addr.checked_add(src.len()) else { return Ok(None) }; - let Some(dst) = self.data.get_mut(addr..end) else { return Ok(None) }; + pub(crate) fn write_all(&mut self, addr: usize, src: &[u8]) -> Option<()> { + let end = addr.checked_add(src.len())?; + let dst = self.data.get_mut(addr..end)?; dst.copy_from_slice(src); - Ok(Some(())) + Some(()) } + /// Fills the range `[addr, addr + len)` with `val`, returning `None` for an invalid range. #[inline(always)] - fn fill(&mut self, addr: usize, len: usize, val: u8) -> Result, crate::Trap> { - let Some(end) = addr.checked_add(len) else { return Ok(None) }; - let Some(dst) = self.data.get_mut(addr..end) else { return Ok(None) }; + pub(crate) fn fill(&mut self, addr: usize, len: usize, val: u8) -> Option<()> { + let end = addr.checked_add(len)?; + let dst = self.data.get_mut(addr..end)?; dst.fill(val); - Ok(Some(())) + Some(()) } + /// Copies `len` bytes from `src` to `dst` within the memory, returning `None` for an invalid + /// range. #[inline(always)] - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result, crate::Trap> { - let Some(src_end) = src.checked_add(len) else { return Ok(None) }; - let Some(dst_end) = dst.checked_add(len) else { return Ok(None) }; + pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Option<()> { + let src_end = src.checked_add(len)?; + let dst_end = dst.checked_add(len)?; if src_end > self.data.len() || dst_end > self.data.len() { - return Ok(None); + return None; } - self.data.copy_within(src..src_end, dst); - Ok(Some(())) - } - - #[inline(always)] - fn read_8(&self, addr: usize) -> core::result::Result<[u8; 1], crate::Trap> { - self.check_fixed_addr::<1>(addr)?; - Ok([self.data[addr]]) - } - - #[inline(always)] - fn read_16(&self, addr: usize) -> core::result::Result<[u8; 2], crate::Trap> { - self.read_fixed::<2>(addr) - } - - #[inline(always)] - fn read_32(&self, addr: usize) -> core::result::Result<[u8; 4], crate::Trap> { - self.read_fixed::<4>(addr) - } - - #[inline(always)] - fn read_64(&self, addr: usize) -> core::result::Result<[u8; 8], crate::Trap> { - self.read_fixed::<8>(addr) - } - - #[inline(always)] - fn read_128(&self, addr: usize) -> core::result::Result<[u8; 16], crate::Trap> { - self.read_fixed::<16>(addr) - } - - #[inline(always)] - fn write_8(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.check_fixed_addr::<1>(addr)?; - self.data[addr] = bytes[0]; - Ok(()) - } - - #[inline(always)] - fn write_16(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.check_fixed_addr::<2>(addr)?; - self.data[addr..addr + 2].copy_from_slice(bytes); - Ok(()) - } - - #[inline(always)] - fn write_32(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.check_fixed_addr::<4>(addr)?; - self.data[addr..addr + 4].copy_from_slice(bytes); - Ok(()) - } - - #[inline(always)] - fn write_64(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.check_fixed_addr::<8>(addr)?; - self.data[addr..addr + 8].copy_from_slice(bytes); - Ok(()) - } - - #[inline(always)] - fn write_128(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.check_fixed_addr::<16>(addr)?; - self.data[addr..addr + 16].copy_from_slice(bytes); - Ok(()) + Some(()) } } diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index 33a549ca..d1dde54f 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -23,7 +23,6 @@ mod types; use const_expr::eval_const; pub(crate) use gc::{decode_data, default_value, pop_value, push_value}; -pub use memory::{LazyLinearMemory, LinearMemory, MemoryBackend, PagedMemory, VecMemory}; pub(crate) use memory::{MemValue, MemoryInstance}; pub(crate) use state::State; pub(crate) use types::{canonicalize_ref_type, canonicalize_value_type}; @@ -32,6 +31,25 @@ pub(crate) use {data::*, element::*, exception::*, function::*, global::*, table // global store id counter static STORE_ID: AtomicU32 = AtomicU32::new(0); +/// Limits resource consumption within a [`Store`]. +/// +/// The limiter is consulted before a memory grows, so a host can bound how much a guest may +/// consume. This mirrors the shape of Wasmtime's `ResourceLimiter`; additional resource types can +/// be added as the need arises. +/// +/// A limiter is shared across the stores created from one [`Engine`], so implementations must be +/// `Send + Sync` and use interior mutability to track state. +pub trait ResourceLimiter: Send + Sync { + /// Notifies the limiter that a linear memory is about to grow. + /// + /// `current` and `desired` are byte sizes and are always multiples of the memory's page size. + /// `maximum` is the memory's declared maximum in bytes, or `None` when the memory is unbounded. + /// + /// Return `Ok(true)` to allow the grow, `Ok(false)` to reject it (the guest observes + /// `memory.grow` returning -1), or `Err` to turn the grow into a trap. + fn memory_growing(&self, current: usize, desired: usize, maximum: Option) -> Result; +} + /// Global state that can be manipulated by WebAssembly programs /// /// Managed WebAssembly GC objects are collected automatically. Other Store @@ -263,12 +281,12 @@ impl Store { pub(crate) fn init_memories( &mut self, memories: &[MemoryType], - init: impl Fn(MemoryType, &MemoryBackend) -> Result, + init: impl Fn(MemoryType) -> Result, ) -> Result> { let start = self.state.memories.len() as MemAddr; self.state.memories.reserve_exact(memories.len()); for mem in memories { - self.state.memories.push(cold_err!(init(*mem, &self.engine.config().memory_backend))?); + self.state.memories.push(cold_err!(init(*mem))?); } Ok(start..start + memories.len() as MemAddr) } @@ -428,7 +446,7 @@ impl Store { }; let offset = usize::try_from(offset).unwrap_or(usize::MAX); - match mem.inner.write_all(offset, &data.data)? { + match mem.inner.write_all(offset, &data.data) { Some(()) => None, None => { return Ok(( diff --git a/crates/tinywasm/tests/memory.rs b/crates/tinywasm/tests/memory.rs new file mode 100644 index 00000000..7e462c91 --- /dev/null +++ b/crates/tinywasm/tests/memory.rs @@ -0,0 +1,121 @@ +extern crate alloc; + +use alloc::sync::Arc; + +use tinywasm::engine::Config; +use tinywasm::types::{MemoryArch, MemoryType}; +use tinywasm::{Engine, Memory, ModuleInstance, ResourceLimiter, Store, Trap}; + +type TestResult = Result>; + +fn store_with_limiter(limiter: Arc) -> Store { + let engine = Engine::new(Config::new().with_resource_limiter(limiter)); + Store::new(engine) +} + +#[test] +fn memory_read_write_roundtrip() -> TestResult { + let mut store = Store::default(); + let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; + + memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4, 5])?; + assert_eq!(memory.read_vec(&store, 0, 5)?, &[1, 2, 3, 4, 5]); + memory.fill(&mut store, 2, 2, 0)?; + assert_eq!(memory.read_vec(&store, 0, 5)?, &[1, 2, 0, 0, 5]); + Ok(()) +} + +#[test] +fn read_returns_short_count_at_end_of_memory() -> TestResult { + let mut store = Store::default(); + let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(4)))?; + memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4])?; + + let mut dst = [9; 8]; + assert_eq!(memory.read(&store, 2, &mut dst)?, 2); + assert_eq!(&dst[..2], &[3, 4]); + assert_eq!(&dst[2..], &[9; 6]); + + Ok(()) +} + +#[test] +fn memory64_default_limit_is_not_memory32_limit() { + let ty = MemoryType::new(MemoryArch::I64, 65_537, None, None); + assert!(ty.page_count_max() > 65_536); +} + +struct DenyAll; + +impl ResourceLimiter for DenyAll { + fn memory_growing(&self, _current: usize, _desired: usize, _maximum: Option) -> Result { + Ok(false) + } +} + +struct TrapAll; + +impl ResourceLimiter for TrapAll { + fn memory_growing(&self, _current: usize, _desired: usize, _maximum: Option) -> Result { + Err(Trap::Other("growth denied")) + } +} + +#[test] +fn resource_limiter_can_reject_growth() -> TestResult { + let mut store = store_with_limiter(Arc::new(DenyAll)); + let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; + + assert_eq!(memory.grow(&mut store, 1)?, None); + assert_eq!(memory.page_count(&store)?, 1); + Ok(()) +} + +#[test] +fn resource_limiter_can_trap_on_growth() -> TestResult { + let mut store = store_with_limiter(Arc::new(TrapAll)); + let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; + + assert!(matches!(memory.grow(&mut store, 1).unwrap_err(), tinywasm::Error::Trap(Trap::Other("growth denied")))); + Ok(()) +} + +#[test] +fn resource_limiter_rejects_guest_memory_grow() -> TestResult { + let wasm = wat::parse_str( + r#" + (module + (memory 1) + (func (export "grow") (result i32) + i32.const 1 + memory.grow)) + "#, + )?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = store_with_limiter(Arc::new(DenyAll)); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + + let grow = instance.func::<(), i32>(&store, "grow")?; + assert_eq!(grow.call(&mut store, ())?, -1); + Ok(()) +} + +#[test] +fn resource_limiter_allows_guest_memory_grow_by_default() -> TestResult { + let wasm = wat::parse_str( + r#" + (module + (memory 1) + (func (export "grow") (result i32) + i32.const 1 + memory.grow)) + "#, + )?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + + let grow = instance.func::<(), i32>(&store, "grow")?; + assert_eq!(grow.call(&mut store, ())?, 1); + Ok(()) +} diff --git a/crates/tinywasm/tests/memory_backends.rs b/crates/tinywasm/tests/memory_backends.rs deleted file mode 100644 index 73657df5..00000000 --- a/crates/tinywasm/tests/memory_backends.rs +++ /dev/null @@ -1,307 +0,0 @@ -extern crate alloc; - -use alloc::sync::Arc; -use core::sync::atomic::{AtomicUsize, Ordering}; - -#[cfg(feature = "std")] -use std::io::{Read, Seek, SeekFrom, Write}; - -use tinywasm::engine::Config; -use tinywasm::types::{MemoryArch, MemoryType}; -use tinywasm::{Engine, Memory, MemoryBackend, Module, ModuleInstance, PagedMemory, Store}; -use tinywasm_parser::{Parser, ParserOptions}; - -type TestResult = Result>; - -fn initial_memory_size(ty: MemoryType) -> usize { - usize::try_from(ty.page_count_initial()) - .ok() - .and_then(|pages| pages.checked_mul(usize::try_from(ty.page_size()).ok()?)) - .expect("test memory size should fit usize") -} - -fn instantiate_module_with_counting_backend(module: Module) -> TestResult { - let created = Arc::new(AtomicUsize::new(0)); - let factory_calls = created.clone(); - let backend = MemoryBackend::custom(move |ty| { - factory_calls.fetch_add(1, Ordering::Relaxed); - PagedMemory::try_new(initial_memory_size(ty), 16) - }); - let engine = Engine::new(Config::new().with_memory_backend(backend)); - let mut store = Store::new(engine); - - let _ = ModuleInstance::instantiate(&mut store, &module, None)?; - - Ok(created.load(Ordering::Relaxed)) -} - -fn instantiate_with_counting_backend(wat: &str) -> TestResult { - let wasm = wat::parse_str(wat)?; - let module = tinywasm::parse_bytes(&wasm)?; - instantiate_module_with_counting_backend(module) -} - -fn instantiate_exported_memory_with_counting_backend( - wat: &str, -) -> TestResult<(Store, tinywasm::ModuleInstance, Arc)> { - let wasm = wat::parse_str(wat)?; - let module = tinywasm::parse_bytes(&wasm)?; - let created = Arc::new(AtomicUsize::new(0)); - let factory_calls = created.clone(); - let backend = MemoryBackend::custom(move |ty| { - factory_calls.fetch_add(1, Ordering::Relaxed); - PagedMemory::try_new(initial_memory_size(ty), 16) - }); - let engine = Engine::new(Config::new().with_memory_backend(backend)); - let mut store = Store::new(engine); - let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - Ok((store, instance, created)) -} - -#[test] -fn paged_backend_works_for_module_memories() -> TestResult { - let wasm = wat::parse_str( - r#" - (module - (memory (export "memory") 1) - ) - "#, - )?; - - let module = tinywasm::parse_bytes(&wasm)?; - let config = Config::new().with_memory_backend(MemoryBackend::paged(8)); - let mut store = Store::new(Engine::new(config)); - let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - let memory = instance.memory("memory")?; - - memory.copy_from_slice(&mut store, 6, &[1, 2, 3, 4, 5, 6, 7, 8])?; - assert_eq!(memory.read_vec(&store, 6, 8)?, &[1, 2, 3, 4, 5, 6, 7, 8]); - - Ok(()) -} - -#[test] -fn custom_backend_factory_is_used_for_host_memories() -> TestResult { - let created = Arc::new(AtomicUsize::new(0)); - let seen_page_size = Arc::new(AtomicUsize::new(0)); - let factory_calls = created.clone(); - let page_size_seen = seen_page_size.clone(); - - let backend = MemoryBackend::custom(move |ty| { - factory_calls.fetch_add(1, Ordering::Relaxed); - page_size_seen.store(ty.page_size() as usize, Ordering::Relaxed); - PagedMemory::try_new(initial_memory_size(ty), 16) - }); - - let engine = Engine::new(Config::new().with_memory_backend(backend)); - let mut store = Store::new(engine); - - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(2), Some(32)))?; - assert_eq!(memory.ty(&store)?.page_size(), 32); - memory.copy_from_slice(&mut store, 12, &[9, 8, 7, 6, 5])?; - - assert_eq!(memory.read_vec(&store, 12, 5)?, &[9, 8, 7, 6, 5]); - assert_eq!(created.load(Ordering::Relaxed), 1); - assert_eq!(seen_page_size.load(Ordering::Relaxed), 32); - - Ok(()) -} - -#[test] -fn local_memory_without_observable_use_is_not_allocated() -> TestResult { - let created = instantiate_with_counting_backend( - r#" - (module - (memory 1) - (func (export "run")) - ) - "#, - )?; - - assert_eq!(created, 0); - Ok(()) -} - -#[test] -fn exported_local_memory_is_not_eagerly_allocated() -> TestResult { - let created = instantiate_with_counting_backend( - r#" - (module - (memory (export "memory") 1) - ) - "#, - )?; - - assert_eq!(created, 0); - Ok(()) -} - -#[test] -fn exported_local_memory_reads_zeroes_without_materializing() -> TestResult { - let (mut store, instance, created) = instantiate_exported_memory_with_counting_backend( - r#" - (module - (memory (export "memory") 1) - ) - "#, - )?; - - let memory = instance.memory("memory")?; - assert_eq!(created.load(Ordering::Relaxed), 0); - assert_eq!(memory.len(&store)?, 65536); - assert_eq!(memory.read_vec(&store, 65534, 2)?, &[0, 0]); - assert_eq!(created.load(Ordering::Relaxed), 0); - - memory.copy_from_slice(&mut store, 65534, &[1, 2])?; - assert_eq!(created.load(Ordering::Relaxed), 1); - assert_eq!(memory.read_vec(&store, 65534, 2)?, &[1, 2]); - Ok(()) -} - -#[test] -fn active_data_segment_on_local_memory_is_allocated() -> TestResult { - let created = instantiate_with_counting_backend( - r#" - (module - (memory 1) - (data (i32.const 0) "hi") - ) - "#, - )?; - - assert_eq!(created, 1); - Ok(()) -} - -#[test] -fn local_memory_instruction_is_allocated() -> TestResult { - let created = instantiate_with_counting_backend( - r#" - (module - (memory 1) - (func (export "run") (drop (memory.size))) - ) - "#, - )?; - - assert_eq!(created, 1); - Ok(()) -} - -#[test] -fn disabled_local_memory_allocation_optimization_keeps_old_behavior() -> TestResult { - let wasm = wat::parse_str( - r#" - (module - (memory 1) - (func (export "run")) - ) - "#, - )?; - let parser = Parser::new(ParserOptions::default().with_local_memory_allocation_optimization(false)); - let module = parser.parse_module_bytes(&wasm)?; - - let created = instantiate_module_with_counting_backend(module)?; - - assert_eq!(created, 1); - Ok(()) -} - -#[test] -fn read_returns_short_count_at_end_of_memory() -> TestResult { - let mut store = Store::default(); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(4)))?; - memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4])?; - - let mut dst = [9; 8]; - assert_eq!(memory.read(&store, 2, &mut dst)?, 2); - assert_eq!(&dst[..2], &[3, 4]); - assert_eq!(&dst[2..], &[9; 6]); - - Ok(()) -} - -#[test] -fn paged_read_and_write_stop_at_chunk_boundaries() -> TestResult { - let engine = Engine::new(Config::new().with_memory_backend(MemoryBackend::paged(4))); - let mut store = Store::new(engine); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(16)))?; - - memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4, 5, 6, 7, 8])?; - - let mut read_buf = [9; 6]; - assert_eq!(memory.read(&store, 2, &mut read_buf)?, 2); - assert_eq!(&read_buf[..2], &[3, 4]); - assert_eq!(&read_buf[2..], &[9; 4]); - - let mut exact_buf = [0; 6]; - memory.read_exact(&store, 2, &mut exact_buf)?; - assert_eq!(exact_buf, [3, 4, 5, 6, 7, 8]); - - assert_eq!(memory.write(&mut store, 6, &[10, 11, 12, 13])?, 2); - assert_eq!(memory.read_vec(&store, 6, 4)?, &[10, 11, 0, 0]); - - memory.copy_from_slice(&mut store, 6, &[20, 21, 22, 23])?; - assert_eq!(memory.read_vec(&store, 6, 4)?, &[20, 21, 22, 23]); - - Ok(()) -} - -#[test] -fn paged_fixed_width_accesses_cross_chunk_boundaries() -> TestResult { - use tinywasm::LinearMemory; - - let mut memory = PagedMemory::try_new(32, 4).map_err(tinywasm::Error::from)?; - memory.write_32(2, &0x1234_5678u32.to_le_bytes()).map_err(tinywasm::Error::from)?; - memory.write_64(6, &0x0123_4567_89ab_cdefu64.to_le_bytes()).map_err(tinywasm::Error::from)?; - memory.write_128(14, &u128::MAX.to_le_bytes()).map_err(tinywasm::Error::from)?; - - assert_eq!(u32::from_le_bytes(memory.read_32(2).map_err(tinywasm::Error::from)?), 0x1234_5678); - assert_eq!(u64::from_le_bytes(memory.read_64(6).map_err(tinywasm::Error::from)?), 0x0123_4567_89ab_cdef); - assert_eq!(u128::from_le_bytes(memory.read_128(14).map_err(tinywasm::Error::from)?), u128::MAX); - Ok(()) -} - -#[test] -fn lazy_custom_backend_creation_trap_is_propagated() -> TestResult { - let wasm = wat::parse_str(r#"(module (memory (export "memory") 1))"#)?; - let module = tinywasm::parse_bytes(&wasm)?; - let backend = MemoryBackend::custom(|_| Err::(tinywasm::Trap::Other("backend unavailable"))); - let mut store = Store::new(Engine::new(Config::new().with_memory_backend(backend))); - let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - let memory = instance.memory("memory")?; - - assert_eq!( - memory.copy_from_slice(&mut store, 0, &[1]).unwrap_err(), - tinywasm::Error::from(tinywasm::Trap::Other("backend unavailable")) - ); - Ok(()) -} - -#[test] -fn memory64_default_limit_is_not_memory32_limit() { - let ty = MemoryType::new(MemoryArch::I64, 65_537, None, None); - assert!(ty.page_count_max() > 65_536); -} - -#[cfg(feature = "std")] -#[test] -fn memory_cursor_supports_read_write_and_seek() -> TestResult { - let mut store = Store::default(); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(8)))?; - - let mut cursor = memory.cursor(&mut store)?; - cursor.seek(SeekFrom::Start(2))?; - cursor.write_all(b"abc")?; - cursor.seek(SeekFrom::Start(0))?; - - let mut buf = [0; 5]; - cursor.read_exact(&mut buf)?; - assert_eq!(buf, [0, 0, b'a', b'b', b'c']); - - cursor.seek(SeekFrom::End(-1))?; - cursor.write_all(b"z")?; - - assert_eq!(memory.read_vec(&store, 0, 8)?, &[0, 0, b'a', b'b', b'c', 0, 0, b'z']); - Ok(()) -} diff --git a/crates/types/src/instructions.rs b/crates/types/src/instructions.rs index 797f7038..c9786d3e 100644 --- a/crates/types/src/instructions.rs +++ b/crates/types/src/instructions.rs @@ -799,89 +799,6 @@ pub enum Instruction { const _: () = assert!(core::mem::size_of::() == 8); -impl Instruction { - /// Returns the largest module-local memory index used by this instruction. - #[inline] - pub fn memory_addr(&self, data: &super::WasmFunctionData) -> Option { - use Instruction::*; - match *self { - IncMemoryLocal32(arg) - | IncMemoryLocal64(arg) - | StoreLocalLocal32(arg) - | StoreLocalLocal64(arg) - | StoreLocalLocal128(arg) - | LoadLocal32(arg) - | LoadLocal64(arg) - | LoadLocal8S32(arg) - | LoadLocal8U32(arg) - | LoadLocal16S32(arg) - | LoadLocal16U32(arg) - | LoadLocalTee32(arg) - | LoadLocalSet32(arg) - | LoadLocalTee8S32(arg) - | LoadLocalTee8U32(arg) - | LoadLocalTee16S32(arg) - | LoadLocalTee16U32(arg) - | LoadLocalSet8S32(arg) - | LoadLocalSet8U32(arg) - | LoadLocalSet16S32(arg) - | LoadLocalSet16U32(arg) - | LoadLocalTee128(arg) - | LoadLocalSet128(arg) => Some(arg.memory_arg_idx.get(data).mem_addr()), - I32Load(index) - | I64Load(index) - | F32Load(index) - | F64Load(index) - | I32Load8S(index) - | I32Load8U(index) - | I32Load16S(index) - | I32Load16U(index) - | I64Load8S(index) - | I64Load8U(index) - | I64Load16S(index) - | I64Load16U(index) - | I64Load32S(index) - | I64Load32U(index) - | I32Store(index) - | I64Store(index) - | F32Store(index) - | F64Store(index) - | I32Store8(index) - | I32Store16(index) - | I64Store8(index) - | I64Store16(index) - | I64Store32(index) - | V128Load(index) - | V128Load8x8S(index) - | V128Load8x8U(index) - | V128Load16x4S(index) - | V128Load16x4U(index) - | V128Load32x2S(index) - | V128Load32x2U(index) - | V128Load8Splat(index) - | V128Load16Splat(index) - | V128Load32Splat(index) - | V128Load64Splat(index) - | V128Load32Zero(index) - | V128Load64Zero(index) - | V128Store(index) => Some(index.get(data).mem_addr()), - V128Load8Lane(arg) | V128Load16Lane(arg) | V128Load32Lane(arg) | V128Load64Lane(arg) - | V128Store8Lane(arg) | V128Store16Lane(arg) | V128Store32Lane(arg) | V128Store64Lane(arg) => { - Some(arg.memory_arg_idx.get(data).mem_addr()) - } - FMaStoreF32(arg) | FMaStoreF64(arg) => Some(arg.mem_addr()), - MemorySize(memory) | MemoryGrow(memory) | MemoryFill(memory) => Some(memory), - MemoryFillConst(index) => Some(index.get(data).memory), - MemoryInit(index) => Some(index.get(data).second), - MemoryCopy(index) => { - let value = index.get(data); - Some(value.first.max(value.second)) - } - _ => None, - } - } -} - #[cfg(test)] mod tests { use alloc::vec; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index bd927539..7d78ed61 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -133,9 +133,6 @@ pub struct ModuleInner { /// /// Corresponds to the `elem` section of the original WebAssembly module. pub elements: Box<[Element]>, - - /// How instantiation should prepare the module's local memories. - pub local_memory_allocation: LocalMemoryAllocation, } impl Module { @@ -297,20 +294,6 @@ pub enum ExportType<'a> { Tag(&'a FuncType), } -/// How instantiation should prepare local memories declared by the module. -#[derive(Clone, Copy, PartialEq, Eq, Default)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub enum LocalMemoryAllocation { - /// The module's local memories are unobservable and can be skipped entirely. - #[default] - Skip, - /// The module's local memories may be observed through exports, but can be delayed until first use. - Lazy, - /// The module's local memories must be allocated during instantiation. - Eager, -} - /// A WebAssembly External Kind. /// /// See @@ -614,6 +597,12 @@ impl MemoryType { } } + /// The declared maximum page count, or `None` when the memory is unbounded. + #[inline] + pub const fn page_count_max_declared(&self) -> Option { + self.page_count_max + } + #[inline] pub const fn page_size(&self) -> u64 { if let Some(page_size) = self.page_size { page_size } else { MEM_PAGE_SIZE } diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index b338bd2c..67010742 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-wasm-examples" -edition = "2021" +edition = "2024" publish = false # treat this as an independent package @@ -22,10 +22,6 @@ path = "src/print.rs" name = "tinywasm" path = "src/tinywasm.rs" -[[bin]] -name = "tinywasm_precompiled" -path = "src/tinywasm_precompiled.rs" - [[bin]] name = "tinywasm_no_std" path = "src/tinywasm_no_std.rs" @@ -41,10 +37,10 @@ path = "src/argon2id.rs" [dependencies] argon2 = { version = "0.5" } dlmalloc = { version = "0.2", features = ["global"] } -tinywasm = { path = "../../crates/tinywasm", default-features = false, features = [ - "archive", - "parser" -] } +tinywasm = { path = "../../crates/tinywasm", default-features = false, features = ["archive", "parser"] } + +[lints.clippy] +missing_safety_doc = "allow" [features] default = ["std"] diff --git a/examples/rust/build.sh b/examples/rust/build.sh index b2a61497..cbd3b276 100755 --- a/examples/rust/build.sh +++ b/examples/rust/build.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash cd "$(dirname "$0")" || exit -bins=("host_fn" "hello" "fibonacci" "print" "tinywasm" "tinywasm_precompiled" "argon2id") -exclude_wat=("tinywasm" "tinywasm_precompiled") +bins=("host_fn" "hello" "fibonacci" "print" "tinywasm" "argon2id") +exclude_wat=("tinywasm") out_dir="./target/wasm32-unknown-unknown/wasm" dest_dir="out" diff --git a/examples/rust/src/hello.rs b/examples/rust/src/hello.rs index 3d9f400d..06ef5c92 100644 --- a/examples/rust/src/hello.rs +++ b/examples/rust/src/hello.rs @@ -19,10 +19,12 @@ pub unsafe extern "C" fn arg_size() -> i32 { #[unsafe(no_mangle)] pub unsafe extern "C" fn hello(len: i32) { - let arg = core::str::from_utf8(&ARG[0..len as usize]).unwrap(); - let res = format!("Hello, {}!", arg).as_bytes().to_vec(); + unsafe { + let arg = core::str::from_utf8(&ARG[0..len as usize]).unwrap(); + let res = format!("Hello, {}!", arg).as_bytes().to_vec(); - let len = res.len() as i32; - let ptr = res.leak().as_ptr() as i64; - print_utf8(ptr, len); + let len = res.len() as i32; + let ptr = res.leak().as_ptr() as i64; + print_utf8(ptr, len); + } } diff --git a/examples/rust/src/print.rs b/examples/rust/src/print.rs index d2934f0f..991ea4a6 100644 --- a/examples/rust/src/print.rs +++ b/examples/rust/src/print.rs @@ -7,5 +7,7 @@ unsafe extern "C" { #[unsafe(no_mangle)] pub unsafe extern "C" fn add_and_print(lh: i32, rh: i32) { - printi32(lh + rh); + unsafe { + printi32(lh + rh); + } } diff --git a/examples/rust/src/print.twasm b/examples/rust/src/print.twasm index f0fa9a87c3b90554afabdd92675a76a7a92f0820..7841329f5f38dbf8f4f2f2998afe4c8f2787a33c 100644 GIT binary patch delta 14 VcmbQmIE!(DI7 tinywasm::Result<()> { - let module = Module::try_from_twasm(include_bytes!("./print.twasm"))?; - let mut store = tinywasm::Store::default(); - - let printi32 = HostFunction::from(|_: FuncContext<'_>, v: i32| { - unsafe { printi32(v) } - Ok(()) - }); - - let mut imports = tinywasm::Imports::new(); - imports.define("env", "printi32", printi32); - - let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; - let add_and_print = instance.func::<(i32, i32), ()>(&store, "add_and_print")?; - add_and_print.call(&mut store, (1, 2))?; - Ok(()) -} diff --git a/examples/wasm-rust.rs b/examples/wasm-rust.rs index b3398f8b..0cfb0cec 100644 --- a/examples/wasm-rust.rs +++ b/examples/wasm-rust.rs @@ -35,7 +35,6 @@ fn main() -> Result<()> { println!(" host_fn"); println!(" fibonacci - calculate fibonacci(30)"); println!(" tinywasm - run printi32 inside of tinywasm inside of itself"); - println!(" tinywasm_precompiled - run a precompiled module inside of tinywasm"); println!(" tinywasm_no_std - run a precompiled module inside of no_std tinywasm"); println!(" argon2id - run argon2id(1000, 2, 1)"); return Ok(()); @@ -46,7 +45,6 @@ fn main() -> Result<()> { "printi32" => printi32()?, "fibonacci" => fibonacci()?, "tinywasm" => tinywasm()?, - "tinywasm_precompiled" => tinywasm_precompiled()?, "tinywasm_no_std" => tinywasm_no_std()?, "argon2id" => argon2id()?, "host_fn" => host_fn()?, @@ -60,8 +58,6 @@ fn main() -> Result<()> { fibonacci()?; println!("\ntinywasm.wasm:"); tinywasm()?; - println!("\ntinywasm_precompiled.wasm:"); - tinywasm_precompiled()?; println!("\ntinywasm_no_std.wasm:"); tinywasm_no_std()?; println!("argon2id.wasm:"); @@ -105,21 +101,6 @@ fn tinywasm_no_std() -> Result<()> { Ok(()) } -fn tinywasm_precompiled() -> Result<()> { - let module = tinywasm::parse_file("./examples/rust/out/tinywasm_precompiled.opt.wasm")?; - let mut store = Store::default(); - - let mut imports = Imports::new(); - imports.define("env", "printi32", HostFunction::from(|_: FuncContext<'_>, _x: i32| Ok(()))); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(black_box(&imports)))?; - - let hello = instance.func::<(), ()>(&store, "hello")?; - hello.call(&mut store, black_box(()))?; - hello.call(&mut store, black_box(()))?; - hello.call(&mut store, black_box(()))?; - Ok(()) -} - fn hello() -> Result<()> { let module = tinywasm::parse_file("./examples/rust/out/hello.opt.wasm")?; let mut store = Store::default(); @@ -231,11 +212,6 @@ mod tests { tinywasm().unwrap(); } - #[test] - fn test_tinywasm_precompiled() { - tinywasm_precompiled().unwrap(); - } - #[test] fn test_tinywasm_no_std() { tinywasm_no_std().unwrap(); From af0f6de95d5dda3294c1d8626f6df57f842037dd Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 23 Aug 2026 12:59:14 +0200 Subject: [PATCH 2/5] chore: update wasmparser Signed-off-by: Henry --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 8 ++++---- crates/parser/src/conversion.rs | 2 +- crates/parser/src/error.rs | 2 +- crates/parser/src/lib.rs | 19 +++++++------------ crates/parser/src/module.rs | 8 +++++--- crates/parser/src/parallel.rs | 2 +- crates/parser/src/visit.rs | 2 +- crates/types/src/lib.rs | 4 ++-- 9 files changed, 36 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c135014..ac0895a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,9 +134,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "shlex", @@ -312,9 +312,9 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "embedded-io" @@ -511,9 +511,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" @@ -962,9 +962,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.256.0" +version = "0.257.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1492381bfd5ea51c2a99a919b676662559925cb8d7490547ec2e14c1ad3eb1" +checksum = "7d8ad9f0a39050867bda22e6486c316e2e52d20a42154d5bc433934bf738d085" dependencies = [ "leb128fmt", "wasmparser", @@ -982,9 +982,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.256.0" +version = "0.257.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60bd825ffedc6cba8a642924ba7ae424afbc47811cffbcb7b92031ec24e59b4c" +checksum = "d92fc335fb6d48f46bda1d8b26b69e28320c15ac3272208333833d6e217e2b4a" dependencies = [ "bitflags", "indexmap", @@ -993,9 +993,9 @@ dependencies = [ [[package]] name = "wast" -version = "256.0.0" +version = "257.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ad42723fc9222da007f05812a3249c9a4ee7af79d497fc2a2079579ad7efe6" +checksum = "3c416ee8044cf88c5280e3d87c322b009525eb25142cf7428f0a90ea88febc3b" dependencies = [ "bumpalo", "leb128fmt", @@ -1006,9 +1006,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.256.0" +version = "1.257.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cc86c54d8011b3202e265bfebada440733ea3a788ffaf46d395f7d2fdeacfb" +checksum = "672e5495f00c5fc6c6b502205bae4708aa4dd13d969754438743afe0a8af0f1d" dependencies = [ "wast", ] diff --git a/Cargo.toml b/Cargo.toml index e678b1e6..764ae53d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ default-members = [".", "crates/parser", "crates/tinywasm", "crates/types"] [workspace.package] version = "0.11.0-pre.0" edition = "2024" -rust-version = "1.95" +rust-version = "1.98" repository = "https://github.com/explodingcamera/tinywasm" license = "MIT OR Apache-2.0" keywords = ["interpreter", "no-std", "tinywasm", "wasm", "webassembly"] @@ -31,9 +31,9 @@ pretty_env_logger = "0.5" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } wasm-testsuite = { version = "0.7" } -wasmparser = { version = "0.256", default-features = false } -wast = "256" -wat = "1.256" +wasmparser = { version = "0.257", default-features = false } +wast = "257" +wat = "1.257" criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support", "rayon"] } diff --git a/crates/parser/src/conversion.rs b/crates/parser/src/conversion.rs index b94ebe66..7292b3c9 100644 --- a/crates/parser/src/conversion.rs +++ b/crates/parser/src/conversion.rs @@ -164,7 +164,7 @@ pub(crate) fn convert_module_code( for (local_index, local) in locals_reader.into_iter().enumerate() { let local = local?; if let Some(validator) = validator.as_mut() { - validator.define_locals(locals_position + local_index, local.0, local.1)?; + validator.define_locals(locals_position + local_index as u64, local.0, local.1)?; } extend_local_types(&mut local_types, local.0, local.1)?; } diff --git a/crates/parser/src/error.rs b/crates/parser/src/error.rs index e4d8abde..892e27bb 100644 --- a/crates/parser/src/error.rs +++ b/crates/parser/src/error.rs @@ -20,7 +20,7 @@ pub enum ParseError { /// The error message message: String, /// The offset in the module where the error occurred - offset: usize, + offset: u64, }, /// An invalid encoding was encountered InvalidEncoding(Encoding), diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index fbe53274..262cc074 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -249,7 +249,7 @@ impl Parser { buffer.truncate(buffer.len() - buffer_offset); buffer_offset = 0; } - let read_bytes = Self::read_more(&mut stream, &mut buffer, hint as usize)?; + let read_bytes = Self::read_more(&mut stream, &mut buffer, hint)?; eof = read_bytes == 0; } wasmparser::Chunk::Parsed { consumed, payload } => { @@ -258,17 +258,12 @@ impl Parser { match payload { wasmparser::Payload::CodeSectionStart { count, range, size } => { - let defer = reader.begin_code_section( - count, - range.clone(), - size, - validator.as_mut(), - &self.options, - )?; + let defer = + reader.begin_code_section(count, range, size, validator.as_mut(), &self.options)?; #[cfg(parallel_parser)] if defer { - deferred_code_section = Some((count, range.end - size as usize, size as usize)); + deferred_code_section = Some((count, size as usize)); } #[cfg(not(parallel_parser))] @@ -284,21 +279,21 @@ impl Parser { buffer_offset += consumed; #[cfg(parallel_parser)] - if let Some((count, body_offset, section_size)) = deferred_code_section { + if let Some((count, section_size)) = deferred_code_section { while buffer.len() - buffer_offset < section_size { let remaining = section_size - (buffer.len() - buffer_offset); let read_bytes = Self::read_more(&mut stream, &mut buffer, remaining)?; if read_bytes == 0 { return Err(ParseError::ParseError { message: "unexpected end-of-file".into(), - offset: body_offset + buffer.len() - buffer_offset, + offset: parser.offset() + (buffer.len() - buffer_offset) as u64, }); } } let section_end = buffer_offset + section_size; let section_bytes = alloc::sync::Arc::<[u8]>::from(buffer[buffer_offset..section_end].to_vec()); - reader.queue_owned_code_section(count, body_offset, section_bytes, validator.as_mut())?; + reader.queue_owned_code_section(count, parser.offset(), section_bytes, validator.as_mut())?; parser.skip_section(); buffer_offset = section_end; continue; diff --git a/crates/parser/src/module.rs b/crates/parser/src/module.rs index 876dc677..c076035e 100644 --- a/crates/parser/src/module.rs +++ b/crates/parser/src/module.rs @@ -330,7 +330,7 @@ impl<'a> ModuleReader<'a> { pub(crate) fn begin_code_section( &mut self, count: u32, - range: Range, + range: Range, size: u32, validator: Option<&mut Validator>, options: &ParserOptions, @@ -434,7 +434,7 @@ impl<'a> ModuleReader<'a> { pub(crate) fn queue_owned_code_section( &mut self, count: u32, - body_offset: usize, + body_offset: u64, section_bytes: Arc<[u8]>, validator: Option<&mut Validator>, ) -> Result<()> { @@ -446,6 +446,8 @@ impl<'a> ModuleReader<'a> { for _ in 0..count { let body_reader = reader.read_reader()?; let body_range = body_reader.range(); + let body_start = (body_range.start - body_offset) as usize; + let body_end = (body_range.end - body_offset) as usize; #[cfg(feature = "validate")] let func_to_validate = { let function = wasmparser::FunctionBody::new(body_reader); @@ -456,7 +458,7 @@ impl<'a> ModuleReader<'a> { self.queue_function( crate::parallel::FunctionBodyInput::Owned(crate::parallel::OwnedFunctionBody { section_bytes: section_bytes.clone(), - body_range: (body_range.start - body_offset)..(body_range.end - body_offset), + body_range: body_start..body_end, body_offset: body_range.start, }), func_to_validate, diff --git a/crates/parser/src/parallel.rs b/crates/parser/src/parallel.rs index 61051dce..9b96c7aa 100644 --- a/crates/parser/src/parallel.rs +++ b/crates/parser/src/parallel.rs @@ -17,7 +17,7 @@ pub(crate) struct OwnedFunctionBody { // function jobs from that section. pub section_bytes: Arc<[u8]>, pub body_range: Range, - pub body_offset: usize, + pub body_offset: u64, } pub(crate) struct PendingFunction<'a> { diff --git a/crates/parser/src/visit.rs b/crates/parser/src/visit.rs index 8315fb51..f48e1cfb 100644 --- a/crates/parser/src/visit.rs +++ b/crates/parser/src/visit.rs @@ -221,7 +221,7 @@ impl<'a> FunctionBuilder<'a> { struct ValidateThenVisit<'a, 'm> { validator: &'a mut FuncValidator, builder: &'a mut FunctionBuilder<'m>, - position: usize, + position: u64, } impl ModuleMetadata { diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 7d78ed61..d1f74534 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -700,7 +700,7 @@ impl From<&ImportKind> for ExternalKind { #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] pub struct Data { pub data: Box<[u8]>, - pub range: Range, + pub range: Range, pub kind: DataKind, } @@ -718,7 +718,7 @@ pub enum DataKind { pub struct Element { pub kind: ElementKind, pub items: Box<[ElementItem]>, - pub range: Range, + pub range: Range, pub ty: RefType, } From 64fdb718f815e898a9726e1c443d5316b3db7146 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 23 Aug 2026 13:17:46 +0200 Subject: [PATCH 3/5] fix: enforce limits on initial memory allocation, cleanup memory instance Signed-off-by: Henry --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 7 +- README.md | 2 +- crates/parser/src/conversion.rs | 19 ++- crates/tinywasm/src/instance.rs | 5 +- crates/tinywasm/src/lib.rs | 3 +- crates/tinywasm/src/reference.rs | 12 +- crates/tinywasm/src/store/memory/instance.rs | 119 ++++++------------- crates/tinywasm/src/store/memory/vec.rs | 38 +++--- crates/tinywasm/src/store/mod.rs | 13 +- crates/tinywasm/tests/memory.rs | 41 +++++-- 11 files changed, 124 insertions(+), 137 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8e78ef58..b1d18d60 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -46,7 +46,7 @@ Linear memory is a contiguous `Vec` allocation owned by a `MemoryInstance`. Fixed-width loads and stores use a single const-generic `read_fixed::` / `write_fixed::` 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. -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`. +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`. 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ee0213..c648d1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane. - Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules. - Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size. -- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory growth. +- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory allocation and growth. ### Changed @@ -27,13 +27,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`. - Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution. - Linear memory now uses a single contiguous `Vec`-backed storage with const-generic fixed-width loads and stores. +- Increased the minimum supported Rust version from 1.95 to 1.98. ### Fixed - Directly defined imports now reject handles from a different `Store`. - Tail calls to host functions now return directly to the caller frame. - Fixed Memory64 bulk-memory operations and optimized stores using the wrong value-stack lane. -- Fixed `memory.init` bounds checks, operand lowering, and local-memory allocation analysis. +- Fixed `memory.init` bounds checks and operand lowering. - Fixed Memory64 default limits and host-size handling, including 32-bit targets. ### Breaking Changes @@ -45,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `ModuleInstanceAddr` to `ModuleInstanceId`. - Removed `HostFunction::ty` and `WasmFunction::ty`. Use `Function::ty(&Store)` for runtime function types. - Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`. -- Removed the pluggable memory backend system (`LinearMemory`, `MemoryBackend`, `VecMemory`, `PagedMemory`, `LazyLinearMemory`, and `Config::with_memory_backend`). Linear memory is always `Vec`-backed. +- 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`. - Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly. ## [0.10.0] - 2026-07-24 diff --git a/README.md b/README.md index 24d49367..ff54da7f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ TinyWasm modules can be compiled to the internal `twasm` bytecode format, which With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments. -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. +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. [^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`. diff --git a/crates/parser/src/conversion.rs b/crates/parser/src/conversion.rs index 7292b3c9..364bae8d 100644 --- a/crates/parser/src/conversion.rs +++ b/crates/parser/src/conversion.rs @@ -151,29 +151,26 @@ pub(crate) fn convert_module_code( ty_idx: u32, options: &ParserOptions, ) -> Result<(FunctionCode, Option, OperatorsReaderAllocations)> { - let locals_reader = func.get_locals_reader()?; - #[cfg(feature = "validate")] - let locals_position = locals_reader.original_position(); + let mut locals_reader = func.get_locals_reader()?; let signature = metadata.signature(ty_idx)?.clone(); let mut local_types = signature.params.clone(); #[cfg(feature = "validate")] let mut validator = validator; - #[cfg(feature = "validate")] - for (local_index, local) in locals_reader.into_iter().enumerate() { - let local = local?; + for _ in 0..locals_reader.get_count() { + #[cfg(feature = "validate")] + let position = locals_reader.original_position(); + let local = locals_reader.read()?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { - validator.define_locals(locals_position + local_index as u64, local.0, local.1)?; + validator.define_locals(position, local.0, local.1)?; } extend_local_types(&mut local_types, local.0, local.1)?; } #[cfg(not(feature = "validate"))] - for local in locals_reader { - let local = local?; - extend_local_types(&mut local_types, local.0, local.1)?; - } + let _ = validator; // maps a local's address to the index in the type's locals array let mut local_addr_map = Vec::with_capacity(local_types.len()); diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index 13b76abe..6700d53f 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -176,7 +176,10 @@ impl ModuleInstance { let imported_funcs = addrs.funcs.len(); addrs.funcs.extend(store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs)); addrs.tags.extend(store.init_tags(&module.tags, &type_addrs)); - addrs.memories.extend(store.init_memories(&module.memory_types, MemoryInstance::new)?); + let limiter = store.engine.config().resource_limiter.clone(); + addrs + .memories + .extend(store.init_memories(&module.memory_types, |ty| MemoryInstance::new(ty, limiter.as_deref()))?); store.init_globals(&mut addrs.globals, &module.globals, &addrs.funcs, &type_addrs)?; addrs.tables.extend(store.init_tables(&module.tables, &addrs.globals, &addrs.funcs, &type_addrs)?); diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 16fdca3b..ab874997 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -55,7 +55,8 @@ //! //! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`] //! and [`engine::Config`] to control stack sizing, fuel accounting, and trap-on-OOM -//! behavior. A [`ResourceLimiter`] can be attached to a [`Store`] to bound memory growth. +//! behavior. A [`ResourceLimiter`] can be attached to the engine configuration to bound memory +//! allocation and growth for stores created from that engine. //! //! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory. //! diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index 4a11622c..ae1b51bb 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -163,7 +163,8 @@ impl Memory { /// Create a new memory in the given store. pub fn new(store: &mut Store, ty: MemoryType) -> Result { let addr = store.state.memories.len() as MemAddr; - store.state.memories.push(MemoryInstance::new(ty)?); + let limiter = store.engine.config().resource_limiter.clone(); + store.state.memories.push(MemoryInstance::new(ty, limiter.as_deref())?); Ok(Self(StoreItem::new(store.id(), addr))) } @@ -230,17 +231,16 @@ impl Memory { /// Reads up to `dst.len()` bytes from memory and returns the number of bytes read. /// - /// Depending on the configured backend, this may return fewer bytes than requested even when - /// more data is available. Use [`Self::read_exact`] or [`Self::read_vec`] when you need a full - /// range. + /// This returns fewer bytes than requested when the range extends past the end of memory. Use + /// [`Self::read_exact`] or [`Self::read_vec`] when you need a full range. pub fn read(&self, store: &Store, offset: usize, dst: &mut [u8]) -> Result { Ok(self.instance(store)?.inner.read(offset, dst)) } /// Writes up to `src.len()` bytes into memory and returns the number of bytes written. /// - /// Depending on the configured backend, this may return fewer bytes than requested even when - /// more space is available. Use [`Self::copy_from_slice`] when you need the full slice written. + /// This returns fewer bytes than requested when the range extends past the end of memory. Use + /// [`Self::copy_from_slice`] when you need the full slice written. pub fn write(&self, store: &mut Store, offset: usize, src: &[u8]) -> Result { Ok(self.instance_mut(store)?.inner.write(offset, src)) } diff --git a/crates/tinywasm/src/store/memory/instance.rs b/crates/tinywasm/src/store/memory/instance.rs index 3fc4c5d7..2ca5bbee 100644 --- a/crates/tinywasm/src/store/memory/instance.rs +++ b/crates/tinywasm/src/store/memory/instance.rs @@ -3,7 +3,6 @@ use tinywasm_types::{MemoryArch, MemoryType}; use crate::{Error, ResourceLimiter, Result, Trap}; use super::{MemoryStorage, memory_oob}; -use core::hint::cold_path; /// A WebAssembly Memory Instance /// @@ -22,25 +21,17 @@ impl core::fmt::Debug for MemoryInstance { } impl MemoryInstance { - const COPY_CHUNK_SIZE: usize = 4 * 1024; - + #[inline] fn host_size(kind: MemoryType, pages: u64) -> Result { - #[cfg(target_pointer_width = "64")] - { - pages - .checked_mul(kind.page_size()) - .map(|size| size as usize) - .ok_or(Error::UnsupportedFeature("memory size exceeds the host address space")) - } + let size = pages + .checked_mul(kind.page_size()) + .ok_or(Error::UnsupportedFeature("memory size exceeds the host address space"))?; + usize::try_from(size).map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space")) + } - #[cfg(not(target_pointer_width = "64"))] - { - let page_size = usize::try_from(kind.page_size()) - .map_err(|_| Error::UnsupportedFeature("memory page size exceeds the host address space"))?; - let pages = usize::try_from(pages) - .map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?; - pages.checked_mul(page_size).ok_or(Error::UnsupportedFeature("memory size exceeds the host address space")) - } + #[inline] + fn maximum_size(kind: MemoryType) -> Option { + kind.page_count_max_declared().map(|pages| Self::host_size(kind, pages).unwrap_or(usize::MAX)) } #[inline(always)] @@ -66,7 +57,7 @@ impl MemoryInstance { } } - pub(crate) fn new(kind: MemoryType) -> Result { + pub(crate) fn new(kind: MemoryType, limiter: Option<&dyn ResourceLimiter>) -> Result { let initial_len = Self::host_size(kind, kind.page_count_initial())?; crate::log::debug!( @@ -75,6 +66,13 @@ impl MemoryInstance { kind.page_size() ); + if initial_len != 0 + && let Some(limiter) = limiter + && !cold_err!(limiter.memory_growing(0, initial_len, Self::maximum_size(kind)))? + { + return cold!(Err(Trap::OutOfMemory.into())); + } + let storage = MemoryStorage::try_new(initial_len)?; Ok(Self { kind, inner: storage, page_count: kind.page_count_initial() as usize }) } @@ -90,47 +88,14 @@ impl MemoryInstance { src: usize, len: usize, ) -> Result<(), Trap> { - fn check_range(mem: &MemoryStorage, addr: usize, len: usize) -> Result<(), crate::Trap> { - let Some(end) = addr.checked_add(len) else { - return cold!(Err(memory_oob(addr, len, mem.len()))); - }; - - if end > mem.len() || end < addr { - return cold!(Err(memory_oob(addr, len, mem.len()))); - } - Ok(()) - } - - check_range(&src_memory.inner, src, len)?; - check_range(&self.inner, dst, len)?; - - if len == 0 { - return Ok(()); - } - - let mut buf = [0u8; Self::COPY_CHUNK_SIZE]; - let mut copied = 0; - while copied < len { - let chunk_len = buf.len().min(len - copied); - src_memory.inner.read_exact(src + copied, &mut buf[..chunk_len]).ok_or_else(|| { - cold_path(); - memory_oob(src + copied, chunk_len, src_memory.inner.len()) - })?; - self.inner.write_all(dst + copied, &buf[..chunk_len]).ok_or_else(|| { - cold_path(); - memory_oob(dst + copied, chunk_len, self.inner.len()) - })?; - copied += chunk_len; - } - + src_memory.inner.checked_range(src, len).ok_or_else(|| cold!(memory_oob(src, len, src_memory.inner.len())))?; + self.inner.checked_range(dst, len).ok_or_else(|| cold!(memory_oob(dst, len, self.inner.len())))?; + self.inner.copy_from(dst, &src_memory.inner, src, len); Ok(()) } pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result<(), Trap> { - self.inner.copy_within(dst, src, len).ok_or_else(|| { - cold_path(); - memory_oob(dst, len, self.inner.len()) - }) + self.inner.copy_within(dst, src, len).ok_or_else(|| cold!(memory_oob(dst, len, self.inner.len()))) } pub(crate) fn grow( @@ -139,50 +104,34 @@ impl MemoryInstance { trap_on_oom: bool, limiter: Option<&dyn ResourceLimiter>, ) -> Result, Trap> { - if pages_delta < 0 { - cold_path(); - crate::log::debug!("memory.grow failed: negative delta {}", pages_delta); - return Ok(None); - } - let current_pages = self.page_count; - let Some(pages_delta) = usize::try_from(pages_delta).ok() else { - return Ok(None); - }; - let Some(new_pages) = current_pages.checked_add(pages_delta) else { - return Ok(None); + let Some(new_pages) = usize::try_from(pages_delta).ok().and_then(|delta| current_pages.checked_add(delta)) + else { + return cold!(Ok(None)); }; let max_pages = self.kind.page_count_max().try_into().unwrap_or(usize::MAX); if new_pages > max_pages { - cold_path(); - crate::log::debug!("memory.grow failed: new_pages={}, max_pages={}", new_pages, max_pages); - return Ok(None); + return cold!({ + crate::log::debug!("memory.grow failed: new_pages={}, max_pages={}", new_pages, max_pages); + Ok(None) + }); } let Ok(new_size) = Self::host_size(self.kind, new_pages as u64) else { - return Ok(None); + return cold!(Ok(None)); }; if new_size == self.inner.len() { return Ok(i64::try_from(current_pages).ok()); } - if let Some(limiter) = limiter { - let maximum = self.kind.page_count_max_declared().and_then(|pages| Self::host_size(self.kind, pages).ok()); - match limiter.memory_growing(self.inner.len(), new_size, maximum) { - Ok(true) => {} - Ok(false) => { - cold_path(); - return Ok(None); - } - Err(trap) => { - cold_path(); - return Err(trap); - } - } + if let Some(limiter) = limiter + && !cold_err!(limiter.memory_growing(self.inner.len(), new_size, Self::maximum_size(self.kind)))? + { + return cold!(Ok(None)); } - if let Err(err) = self.inner.grow_to(new_size) { + if let Err(err) = cold_err!(self.inner.grow_to(new_size)) { if trap_on_oom { return Err(err); } diff --git a/crates/tinywasm/src/store/memory/vec.rs b/crates/tinywasm/src/store/memory/vec.rs index 1b90e90e..98c24d6b 100644 --- a/crates/tinywasm/src/store/memory/vec.rs +++ b/crates/tinywasm/src/store/memory/vec.rs @@ -1,4 +1,5 @@ use alloc::vec::Vec; +use core::ops::Range; use super::memory_oob; @@ -25,6 +26,12 @@ impl VecMemory { self.data.len() } + #[inline(always)] + pub(super) fn checked_range(&self, addr: usize, len: usize) -> Option> { + let end = addr.checked_add(len)?; + (end <= self.data.len()).then_some(addr..end) + } + /// Grows the backing allocation to `new_len`. Only called after the Wasm limits and any user /// limiter have accepted the grow. #[inline(always)] @@ -54,7 +61,7 @@ impl VecMemory { /// Writes exactly `N` bytes from `bytes` at `addr`. #[inline(always)] - pub(crate) fn write_fixed(&mut self, addr: usize, bytes: &[u8]) -> Result<(), crate::Trap> { + pub(crate) fn write_fixed(&mut self, addr: usize, bytes: &[u8; N]) -> Result<(), crate::Trap> { self.check_fixed_addr::(addr)?; self.data[addr..addr + N].copy_from_slice(bytes); Ok(()) @@ -85,7 +92,7 @@ impl VecMemory { /// Reads exactly `dst.len()` bytes starting at `addr`, returning `None` for an invalid range. #[inline(always)] pub(crate) fn read_exact(&self, addr: usize, dst: &mut [u8]) -> Option<()> { - dst.copy_from_slice(self.data.get(addr..addr.checked_add(dst.len())?)?); + dst.copy_from_slice(&self.data[self.checked_range(addr, dst.len())?]); Some(()) } @@ -93,24 +100,22 @@ impl VecMemory { /// invalid range. #[inline(always)] pub(crate) fn read_vec(&self, addr: usize, len: usize) -> Option> { - Some(self.data.get(addr..addr.checked_add(len)?)?.to_vec()) + Some(self.data[self.checked_range(addr, len)?].to_vec()) } /// Writes all of `src` at `addr`, returning `None` for an invalid range. #[inline(always)] pub(crate) fn write_all(&mut self, addr: usize, src: &[u8]) -> Option<()> { - let end = addr.checked_add(src.len())?; - let dst = self.data.get_mut(addr..end)?; - dst.copy_from_slice(src); + let range = self.checked_range(addr, src.len())?; + self.data[range].copy_from_slice(src); Some(()) } /// Fills the range `[addr, addr + len)` with `val`, returning `None` for an invalid range. #[inline(always)] pub(crate) fn fill(&mut self, addr: usize, len: usize, val: u8) -> Option<()> { - let end = addr.checked_add(len)?; - let dst = self.data.get_mut(addr..end)?; - dst.fill(val); + let range = self.checked_range(addr, len)?; + self.data[range].fill(val); Some(()) } @@ -118,12 +123,15 @@ impl VecMemory { /// range. #[inline(always)] pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Option<()> { - let src_end = src.checked_add(len)?; - let dst_end = dst.checked_add(len)?; - if src_end > self.data.len() || dst_end > self.data.len() { - return None; - } - self.data.copy_within(src..src_end, dst); + let src = self.checked_range(src, len)?; + self.checked_range(dst, len)?; + self.data.copy_within(src, dst); Some(()) } + + /// Copies a previously checked range from another memory. + #[inline(always)] + pub(super) fn copy_from(&mut self, dst: usize, src_memory: &Self, src: usize, len: usize) { + self.data[dst..dst + len].copy_from_slice(&src_memory.data[src..src + len]); + } } diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index d1dde54f..427f3737 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -33,20 +33,21 @@ static STORE_ID: AtomicU32 = AtomicU32::new(0); /// Limits resource consumption within a [`Store`]. /// -/// The limiter is consulted before a memory grows, so a host can bound how much a guest may -/// consume. This mirrors the shape of Wasmtime's `ResourceLimiter`; additional resource types can -/// be added as the need arises. +/// The limiter is consulted before a memory is created or grown, so a host can bound how much a +/// guest may consume. This mirrors the shape of Wasmtime's `ResourceLimiter`; additional resource +/// types can be added as the need arises. /// /// A limiter is shared across the stores created from one [`Engine`], so implementations must be /// `Send + Sync` and use interior mutability to track state. pub trait ResourceLimiter: Send + Sync { - /// Notifies the limiter that a linear memory is about to grow. + /// Notifies the limiter that a linear memory is about to be allocated or grown. /// /// `current` and `desired` are byte sizes and are always multiples of the memory's page size. /// `maximum` is the memory's declared maximum in bytes, or `None` when the memory is unbounded. /// - /// Return `Ok(true)` to allow the grow, `Ok(false)` to reject it (the guest observes - /// `memory.grow` returning -1), or `Err` to turn the grow into a trap. + /// For initial allocation, `current` is zero. Return `Ok(true)` to allow the allocation or grow, + /// `Ok(false)` to reject it, or `Err` to return the supplied trap. A rejected initial allocation + /// returns [`Trap::OutOfMemory`], while a rejected `memory.grow` returns -1 to the guest. fn memory_growing(&self, current: usize, desired: usize, maximum: Option) -> Result; } diff --git a/crates/tinywasm/tests/memory.rs b/crates/tinywasm/tests/memory.rs index 7e462c91..23f525aa 100644 --- a/crates/tinywasm/tests/memory.rs +++ b/crates/tinywasm/tests/memory.rs @@ -53,17 +53,25 @@ impl ResourceLimiter for DenyAll { } } -struct TrapAll; +struct DenyGrowth; -impl ResourceLimiter for TrapAll { - fn memory_growing(&self, _current: usize, _desired: usize, _maximum: Option) -> Result { - Err(Trap::Other("growth denied")) +impl ResourceLimiter for DenyGrowth { + fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { + Ok(current == 0) + } +} + +struct TrapGrowth; + +impl ResourceLimiter for TrapGrowth { + fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { + if current == 0 { Ok(true) } else { Err(Trap::Other("growth denied")) } } } #[test] fn resource_limiter_can_reject_growth() -> TestResult { - let mut store = store_with_limiter(Arc::new(DenyAll)); + let mut store = store_with_limiter(Arc::new(DenyGrowth)); let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; assert_eq!(memory.grow(&mut store, 1)?, None); @@ -73,7 +81,7 @@ fn resource_limiter_can_reject_growth() -> TestResult { #[test] fn resource_limiter_can_trap_on_growth() -> TestResult { - let mut store = store_with_limiter(Arc::new(TrapAll)); + let mut store = store_with_limiter(Arc::new(TrapGrowth)); let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; assert!(matches!(memory.grow(&mut store, 1).unwrap_err(), tinywasm::Error::Trap(Trap::Other("growth denied")))); @@ -92,7 +100,7 @@ fn resource_limiter_rejects_guest_memory_grow() -> TestResult { "#, )?; let module = tinywasm::parse_bytes(&wasm)?; - let mut store = store_with_limiter(Arc::new(DenyAll)); + let mut store = store_with_limiter(Arc::new(DenyGrowth)); let instance = ModuleInstance::instantiate(&mut store, &module, None)?; let grow = instance.func::<(), i32>(&store, "grow")?; @@ -100,6 +108,25 @@ fn resource_limiter_rejects_guest_memory_grow() -> TestResult { Ok(()) } +#[test] +fn resource_limiter_rejects_host_memory_initial_size() { + let mut store = store_with_limiter(Arc::new(DenyAll)); + let result = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None)); + + assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory)))); +} + +#[test] +fn resource_limiter_rejects_module_memory_initial_size() -> TestResult { + let wasm = wat::parse_str("(module (memory 1))")?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = store_with_limiter(Arc::new(DenyAll)); + let result = ModuleInstance::instantiate(&mut store, &module, None); + + assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory)))); + Ok(()) +} + #[test] fn resource_limiter_allows_guest_memory_grow_by_default() -> TestResult { let wasm = wat::parse_str( From c8c38bc9321d7164c46d4118dc9fe394c707bda5 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 23 Aug 2026 13:32:41 +0200 Subject: [PATCH 4/5] chore: improve ResourceLimiter Signed-off-by: Henry --- CHANGELOG.md | 1 + README.md | 2 +- crates/cli/src/engine_flags.rs | 8 --- crates/tinywasm/src/engine.rs | 18 +----- crates/tinywasm/src/interpreter/executor.rs | 3 +- crates/tinywasm/src/lib.rs | 6 +- crates/tinywasm/src/reference.rs | 7 ++- crates/tinywasm/src/store/memory/instance.rs | 10 +--- crates/tinywasm/src/store/mod.rs | 62 +++++++++++++------- crates/tinywasm/tests/memory.rs | 27 ++++++--- 10 files changed, 76 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c648d1db..3c9f4c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`. - 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`. - Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly. +- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory allocation or growth request. ## [0.10.0] - 2026-07-24 diff --git a/README.md b/README.md index ff54da7f..9d46aaf5 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ TinyWasm modules can be compiled to the internal `twasm` bytecode format, which With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments. -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. +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. [^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`. diff --git a/crates/cli/src/engine_flags.rs b/crates/cli/src/engine_flags.rs index 9c5c068b..edc56aa0 100644 --- a/crates/cli/src/engine_flags.rs +++ b/crates/cli/src/engine_flags.rs @@ -8,10 +8,6 @@ pub struct EngineFlags { #[arg(long, value_enum)] pub fuel_policy: Option, - /// Trap immediately on memory or stack allocation failure - #[arg(long)] - pub trap_on_oom: bool, - /// Fixed value stack size for all value lanes #[arg(long, conflicts_with = "value_stack_dynamic")] pub value_stack_size: Option, @@ -88,10 +84,6 @@ impl EngineFlags { config = config.with_call_stack(call_stack_dynamic.into_stack_config()); } - if self.trap_on_oom { - config = config.with_trap_on_oom(true); - } - Ok(Engine::new(config)) } } diff --git a/crates/tinywasm/src/engine.rs b/crates/tinywasm/src/engine.rs index b394d0f7..f1feae6e 100644 --- a/crates/tinywasm/src/engine.rs +++ b/crates/tinywasm/src/engine.rs @@ -96,8 +96,7 @@ impl StackConfig { /// .with_value_stack_32(StackConfig::dynamic(1024, 36 * 1024)) /// .with_value_stack_64(StackConfig::dynamic(1024, 32 * 1024)) /// .with_value_stack_128(StackConfig::dynamic(256, 4 * 1024)) -/// .with_call_stack(StackConfig::dynamic(64, 1024)) -/// .with_trap_on_oom(true); +/// .with_call_stack(StackConfig::dynamic(64, 1024)); /// /// assert!(matches!(config.fuel_policy(), FuelPolicy::Weighted)); /// ``` @@ -117,9 +116,6 @@ pub struct Config { pub call_stack: StackConfig, /// Fuel accounting policy used by budgeted execution. Defaults to [`FuelPolicy::PerInstruction`]. pub fuel_policy: FuelPolicy, - /// Whether memory and stack allocation failures should trap instead of degrading into normal operation failure modes. - /// Defaults to `false`. - pub trap_on_oom: bool, /// Resource limiter shared across all stores created from this engine. Defaults to `None`. pub resource_limiter: Option>, /// Initial number of GC heap bytes that triggers collection. @@ -171,12 +167,6 @@ impl Config { self } - /// Configure whether memory and stack allocation failures trap immediately. - pub fn with_trap_on_oom(mut self, trap_on_oom: bool) -> Self { - self.trap_on_oom = trap_on_oom; - self - } - /// Set the resource limiter shared across all stores created from this engine. pub fn with_resource_limiter(mut self, limiter: Arc) -> Self { self.resource_limiter = Some(limiter); @@ -193,10 +183,6 @@ impl Config { pub fn fuel_policy(&self) -> FuelPolicy { self.fuel_policy } - - pub(crate) const fn trap_on_oom(&self) -> bool { - self.trap_on_oom - } } impl Default for Config { @@ -207,7 +193,6 @@ impl Default for Config { value_stack_128: StackConfig::fixed(DEFAULT_VALUE_STACK_128_SIZE), call_stack: StackConfig::fixed(DEFAULT_MAX_CALL_STACK_SIZE), fuel_policy: FuelPolicy::default(), - trap_on_oom: false, resource_limiter: None, gc_collection_threshold: 1024 * 1024, } @@ -223,7 +208,6 @@ impl core::fmt::Debug for Config { .field("value_stack_128", &self.value_stack_128) .field("call_stack", &self.call_stack) .field("fuel_policy", &self.fuel_policy) - .field("trap_on_oom", &self.trap_on_oom) .field("resource_limiter", &self.resource_limiter.is_some()) .field("gc_collection_threshold", &self.gc_collection_threshold) .finish() diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index bacbe600..75f12605 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -1656,11 +1656,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { true => ::stack_pop(&mut self.store.value_stack), false => i64::from(::stack_pop(&mut self.store.value_stack)), }; - let trap_on_oom = self.store.engine.config().trap_on_oom(); let limiter = self.store.engine.config().resource_limiter.as_deref(); let mem = self.store.state.get_mem_mut(mem_addr); - let size = mem.grow(pages_delta, trap_on_oom, limiter)?.unwrap_or(-1); + let size = mem.grow(pages_delta, limiter)?.unwrap_or(-1); match is_64bit { true => self.store.value_stack.push::(size)?, false => self.store.value_stack.push::(size as i32)?, diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index ab874997..66ddd081 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -54,9 +54,9 @@ //! ``` //! //! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`] -//! and [`engine::Config`] to control stack sizing, fuel accounting, and trap-on-OOM -//! behavior. A [`ResourceLimiter`] can be attached to the engine configuration to bound memory -//! allocation and growth for stores created from that engine. +//! and [`engine::Config`] to control stack sizing and fuel accounting. A [`ResourceLimiter`] can be +//! attached to the engine configuration to bound memory allocation and growth for stores created +//! from that engine and to trap rejected requests. //! //! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory. //! diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index ae1b51bb..4b637724 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -263,11 +263,14 @@ impl Memory { }) } - /// Grow the memory by the given number of pages. + /// Grows the memory by the given number of pages. + /// + /// Returns the previous size, or `None` if growth fails or is rejected by the resource limiter. + /// A limiter-provided trap is returned as an error. pub fn grow(&self, store: &mut Store, delta_pages: i64) -> Result> { let limiter = store.engine.config().resource_limiter.clone(); let mem = self.instance_mut(store)?; - mem.grow(delta_pages, true, limiter.as_deref()).map_err(Into::into) + mem.grow(delta_pages, limiter.as_deref()).map_err(Into::into) } /// Get the current size of the memory in pages. diff --git a/crates/tinywasm/src/store/memory/instance.rs b/crates/tinywasm/src/store/memory/instance.rs index 2ca5bbee..a075d259 100644 --- a/crates/tinywasm/src/store/memory/instance.rs +++ b/crates/tinywasm/src/store/memory/instance.rs @@ -68,7 +68,7 @@ impl MemoryInstance { if initial_len != 0 && let Some(limiter) = limiter - && !cold_err!(limiter.memory_growing(0, initial_len, Self::maximum_size(kind)))? + && !limiter.memory_growing(0, initial_len, Self::maximum_size(kind))? { return cold!(Err(Trap::OutOfMemory.into())); } @@ -101,7 +101,6 @@ impl MemoryInstance { pub(crate) fn grow( &mut self, pages_delta: i64, - trap_on_oom: bool, limiter: Option<&dyn ResourceLimiter>, ) -> Result, Trap> { let current_pages = self.page_count; @@ -126,15 +125,12 @@ impl MemoryInstance { } if let Some(limiter) = limiter - && !cold_err!(limiter.memory_growing(self.inner.len(), new_size, Self::maximum_size(self.kind)))? + && !limiter.memory_growing(self.inner.len(), new_size, Self::maximum_size(self.kind))? { return cold!(Ok(None)); } - if let Err(err) = cold_err!(self.inner.grow_to(new_size)) { - if trap_on_oom { - return Err(err); - } + if cold_err!(self.inner.grow_to(new_size)).is_err() { return Ok(None); } self.page_count = new_pages; diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index 427f3737..e15ceb99 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -31,32 +31,54 @@ pub(crate) use {data::*, element::*, exception::*, function::*, global::*, table // global store id counter static STORE_ID: AtomicU32 = AtomicU32::new(0); -/// Limits resource consumption within a [`Store`]. +/// Controls resource usage by WebAssembly instances. /// -/// The limiter is consulted before a memory is created or grown, so a host can bound how much a -/// guest may consume. This mirrors the shape of Wasmtime's `ResourceLimiter`; additional resource -/// types can be added as the need arises. +/// Configure a limiter with +/// [`Config::with_resource_limiter`](crate::engine::Config::with_resource_limiter). It currently +/// controls guest linear-memory allocation and growth. It does not account for stacks, GC storage, +/// runtime metadata, or other host allocations. /// -/// A limiter is shared across the stores created from one [`Engine`], so implementations must be -/// `Send + Sync` and use interior mutability to track state. +/// # Example +/// ```rust +/// use std::sync::Arc; +/// use tinywasm::engine::Config; +/// use tinywasm::types::{MemoryArch, MemoryType}; +/// use tinywasm::{Engine, Memory, ResourceLimiter, Store}; +/// +/// struct MemoryLimit(usize); +/// +/// impl ResourceLimiter for MemoryLimit { +/// fn memory_growing( +/// &self, +/// _current: usize, +/// desired: usize, +/// _maximum: Option, +/// ) -> Result { +/// Ok(desired <= self.0) +/// } +/// } +/// +/// let config = Config::new().with_resource_limiter(Arc::new(MemoryLimit(64 * 1024))); +/// let mut store = Store::new(Engine::new(config)); +/// let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; +/// assert_eq!(memory.grow(&mut store, 1)?, None); +/// # Ok::<(), tinywasm::Error>(()) +/// ``` pub trait ResourceLimiter: Send + Sync { - /// Notifies the limiter that a linear memory is about to be allocated or grown. - /// - /// `current` and `desired` are byte sizes and are always multiples of the memory's page size. - /// `maximum` is the memory's declared maximum in bytes, or `None` when the memory is unbounded. + /// Returns whether a memory allocation or growth is allowed. /// - /// For initial allocation, `current` is zero. Return `Ok(true)` to allow the allocation or grow, - /// `Ok(false)` to reject it, or `Err` to return the supplied trap. A rejected initial allocation - /// returns [`Trap::OutOfMemory`], while a rejected `memory.grow` returns -1 to the guest. - fn memory_growing(&self, current: usize, desired: usize, maximum: Option) -> Result; + /// Sizes are in bytes. `current` is zero for initial allocation, and `maximum` is `None` for an + /// unbounded memory. `Ok(false)` rejects the request, while `Err` traps. Rejected initial + /// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. + fn memory_growing( + &self, + current: usize, + desired: usize, + maximum: Option, + ) -> core::result::Result; } -/// Global state that can be manipulated by WebAssembly programs -/// -/// Managed WebAssembly GC objects are collected automatically. Other Store -/// instances, such as modules, functions, memories, and tables, live until the -/// Store is dropped. GC references exposed through the copyable host value API -/// are retained for the Store's lifetime. +/// Runtime state used by WebAssembly instances and host functions. /// /// ## Example /// ```rust diff --git a/crates/tinywasm/tests/memory.rs b/crates/tinywasm/tests/memory.rs index 23f525aa..74491034 100644 --- a/crates/tinywasm/tests/memory.rs +++ b/crates/tinywasm/tests/memory.rs @@ -65,7 +65,7 @@ struct TrapGrowth; impl ResourceLimiter for TrapGrowth { fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { - if current == 0 { Ok(true) } else { Err(Trap::Other("growth denied")) } + if current == 0 { Ok(true) } else { Err(Trap::Unreachable) } } } @@ -80,16 +80,27 @@ fn resource_limiter_can_reject_growth() -> TestResult { } #[test] -fn resource_limiter_can_trap_on_growth() -> TestResult { - let mut store = store_with_limiter(Arc::new(TrapGrowth)); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; +fn resource_limiter_rejects_guest_memory_grow() -> TestResult { + let wasm = wat::parse_str( + r#" + (module + (memory 1) + (func (export "grow") (result i32) + i32.const 1 + memory.grow)) + "#, + )?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = store_with_limiter(Arc::new(DenyGrowth)); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - assert!(matches!(memory.grow(&mut store, 1).unwrap_err(), tinywasm::Error::Trap(Trap::Other("growth denied")))); + let grow = instance.func::<(), i32>(&store, "grow")?; + assert_eq!(grow.call(&mut store, ())?, -1); Ok(()) } #[test] -fn resource_limiter_rejects_guest_memory_grow() -> TestResult { +fn resource_limiter_can_trap_guest_memory_grow() -> TestResult { let wasm = wat::parse_str( r#" (module @@ -100,11 +111,11 @@ fn resource_limiter_rejects_guest_memory_grow() -> TestResult { "#, )?; let module = tinywasm::parse_bytes(&wasm)?; - let mut store = store_with_limiter(Arc::new(DenyGrowth)); + let mut store = store_with_limiter(Arc::new(TrapGrowth)); let instance = ModuleInstance::instantiate(&mut store, &module, None)?; let grow = instance.func::<(), i32>(&store, "grow")?; - assert_eq!(grow.call(&mut store, ())?, -1); + assert!(matches!(grow.call(&mut store, ()), Err(tinywasm::Error::Trap(Trap::Unreachable)))); Ok(()) } From 9a3d487d35fe5f1dc25f53cb7ef97178c1e3ccb0 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 23 Aug 2026 13:40:19 +0200 Subject: [PATCH 5/5] feat: add table_growing to ResourceLimiter for consistency Signed-off-by: Henry --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 5 ++- README.md | 2 +- crates/tinywasm/src/interpreter/executor.rs | 13 ++++--- crates/tinywasm/src/lib.rs | 4 +- crates/tinywasm/src/reference.rs | 21 +++++++--- crates/tinywasm/src/store/mod.rs | 35 +++++++++++++---- crates/tinywasm/src/store/table.rs | 41 ++++++++++++++++---- crates/tinywasm/tests/internal_refs.rs | 2 +- crates/tinywasm/tests/memory.rs | 43 ++++++++++++++++++++- 10 files changed, 132 insertions(+), 36 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b1d18d60..62fa5030 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -46,7 +46,7 @@ Linear memory is a contiguous `Vec` allocation owned by a `MemoryInstance`. Fixed-width loads and stores use a single const-generic `read_fixed::` / `write_fixed::` 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. -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`. +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`. 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9f4c29..c6c8ff70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane. - Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules. - Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size. -- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory allocation and growth. +- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory and table allocation and growth. ### Changed @@ -48,7 +48,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`. - 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`. - Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly. -- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory allocation or growth request. +- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory or table allocation or growth request. +- `Table::grow` now returns `Result>`, matching `Memory::grow`. Growth limits and allocation failures return `None`, while limiter-provided traps return an error. ## [0.10.0] - 2026-07-24 diff --git a/README.md b/README.md index 9d46aaf5..7f27ea4d 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ TinyWasm modules can be compiled to the internal `twasm` bytecode format, which With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments. -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. +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. [^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`. diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index 75f12605..b203980e 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -1880,14 +1880,15 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let arch = self.store.state.get_table(table_addr).kind.arch(); let n = self.pop_table_operand(arch)?; let val = ::stack_pop(&mut self.store.value_stack); + let limiter = self.store.engine.config().resource_limiter.as_deref(); let table = self.store.state.get_table_mut(table_addr); let sz = table.size(); - let result = table.grow(n, val); - match (arch, result) { - (MemoryArch::I32, Ok(())) => self.store.value_stack.push(sz as i32), - (MemoryArch::I32, Err(_)) => self.store.value_stack.push(-1_i32), - (MemoryArch::I64, Ok(())) => self.store.value_stack.push(sz as i64), - (MemoryArch::I64, Err(_)) => self.store.value_stack.push(-1_i64), + let grew = table.grow(n, val, limiter)?; + match (arch, grew) { + (MemoryArch::I32, true) => self.store.value_stack.push(sz as i32), + (MemoryArch::I32, false) => self.store.value_stack.push(-1_i32), + (MemoryArch::I64, true) => self.store.value_stack.push(sz as i64), + (MemoryArch::I64, false) => self.store.value_stack.push(-1_i64), } } diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 66ddd081..bd80ef16 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -55,8 +55,8 @@ //! //! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`] //! and [`engine::Config`] to control stack sizing and fuel accounting. A [`ResourceLimiter`] can be -//! attached to the engine configuration to bound memory allocation and growth for stores created -//! from that engine and to trap rejected requests. +//! attached to the engine configuration to bound memory and table allocation and growth for stores +//! created from that engine and to trap rejected requests. //! //! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory. //! diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index 4b637724..271e71c0 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -373,8 +373,9 @@ impl Table { return Err(Error::other("host tables cannot use module-relative concrete reference types")); } let init = table_value_to_element(&store.state, ty.element_type, init).map_err(Error::from)?; + let limiter = store.engine.config().resource_limiter.clone(); let addr = store.state.tables.len() as TableAddr; - store.state.tables.push(TableInstance::new(ty, init)?); + store.state.tables.push(TableInstance::new(ty, init, limiter.as_deref())?); Ok(Self(StoreItem::new(store.id(), addr))) } @@ -435,15 +436,23 @@ impl Table { store.state.get_table_mut(self.0.addr).copy_within(dst, src, len) } - /// Grow the table and return the previous size. - pub fn grow(&self, store: &mut Store, delta: i32, init: WasmValue) -> Result { + /// Grows the table and returns the previous size. + /// + /// Returns `None` if growth fails or is rejected by the resource limiter. A limiter-provided + /// trap is returned as an error. + pub fn grow(&self, store: &mut Store, delta: i32, init: WasmValue) -> Result> { self.0.validate_store(store)?; let table = store.state.get_table(self.0.addr); let old_size = table.size(); let init = table_value_to_element(&store.state, table.kind.element_type, init)?; - let delta = usize::try_from(delta).map_err(|_| Trap::TableOutOfBounds { offset: 0, len: 1, max: old_size })?; - store.state.get_table_mut(self.0.addr).grow(delta, init)?; - Ok(old_size) + let Ok(delta) = usize::try_from(delta) else { + return Ok(None); + }; + let limiter = store.engine.config().resource_limiter.clone(); + match store.state.get_table_mut(self.0.addr).grow(delta, init, limiter.as_deref())? { + true => Ok(Some(old_size)), + false => Ok(None), + } } } diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index e15ceb99..f0e426fc 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -35,8 +35,8 @@ static STORE_ID: AtomicU32 = AtomicU32::new(0); /// /// Configure a limiter with /// [`Config::with_resource_limiter`](crate::engine::Config::with_resource_limiter). It currently -/// controls guest linear-memory allocation and growth. It does not account for stacks, GC storage, -/// runtime metadata, or other host allocations. +/// controls guest linear-memory and table allocation and growth. It does not account for stacks, +/// GC storage, runtime metadata, or other host allocations. /// /// # Example /// ```rust @@ -69,13 +69,31 @@ pub trait ResourceLimiter: Send + Sync { /// /// Sizes are in bytes. `current` is zero for initial allocation, and `maximum` is `None` for an /// unbounded memory. `Ok(false)` rejects the request, while `Err` traps. Rejected initial - /// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. + /// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. The + /// default implementation allows the request. fn memory_growing( &self, - current: usize, - desired: usize, - maximum: Option, - ) -> core::result::Result; + _current: usize, + _desired: usize, + _maximum: Option, + ) -> core::result::Result { + Ok(true) + } + + /// Returns whether a table allocation or growth is allowed. + /// + /// Sizes are in elements. `current` is zero for initial allocation, and `maximum` is `None` for + /// an unbounded table. `Ok(false)` rejects the request, while `Err` traps. Rejected initial + /// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. The + /// default implementation allows the request. + fn table_growing( + &self, + _current: usize, + _desired: usize, + _maximum: Option, + ) -> core::result::Result { + Ok(true) + } } /// Runtime state used by WebAssembly instances and host functions. @@ -281,6 +299,7 @@ impl Store { type_addrs: &[TypeAddr], ) -> Result> { let start = self.state.tables.len() as TableAddr; + let limiter = self.engine.config().resource_limiter.clone(); self.state.tables.reserve_exact(tables.len()); for table in tables { let init = match &table.init { @@ -295,7 +314,7 @@ impl Store { MemoryArch::I32 => TableType::new(element_type, table.ty.size_initial, table.ty.size_max), MemoryArch::I64 => TableType::new64(element_type, table.ty.size_initial, table.ty.size_max), }; - self.state.tables.push(TableInstance::new(ty, init)?); + self.state.tables.push(TableInstance::new(ty, init, limiter.as_deref())?); } Ok(start..start + tables.len() as TableAddr) } diff --git a/crates/tinywasm/src/store/table.rs b/crates/tinywasm/src/store/table.rs index 8e6c0551..da526605 100644 --- a/crates/tinywasm/src/store/table.rs +++ b/crates/tinywasm/src/store/table.rs @@ -1,4 +1,4 @@ -use crate::{Result, Trap, interpreter::ValueRef}; +use crate::{ResourceLimiter, Result, Trap, interpreter::ValueRef}; use alloc::vec::Vec; use core::ops::Range; use tinywasm_types::*; @@ -16,11 +16,17 @@ pub(crate) struct TableInstance { impl TableInstance { /// Creates a table filled with the given initial reference. - pub(crate) fn new(kind: TableType, init: ValueRef) -> Result { + pub(crate) fn new(kind: TableType, init: ValueRef, limiter: Option<&dyn ResourceLimiter>) -> Result { let size = cold_err!(usize::try_from(kind.size_initial)).map_err(|_| Trap::OutOfMemory)?; if size > MAX_TABLE_SIZE { return Err(Trap::OutOfMemory.into()); } + if size != 0 + && let Some(limiter) = limiter + && !limiter.table_growing(0, size, Self::maximum_size(kind))? + { + return Err(Trap::OutOfMemory.into()); + } let mut elements = Vec::new(); cold_err!(elements.try_reserve_exact(size)).map_err(|_| Trap::OutOfMemory)?; elements.resize(size, init); @@ -74,17 +80,38 @@ impl TableInstance { Ok(()) } - pub(crate) fn grow(&mut self, n: usize, init: ValueRef) -> Result<(), Trap> { - let len = n.checked_add(self.elements.len()).ok_or(Trap::OutOfMemory)?; + pub(crate) fn grow( + &mut self, + n: usize, + init: ValueRef, + limiter: Option<&dyn ResourceLimiter>, + ) -> Result { + let Some(len) = n.checked_add(self.elements.len()) else { + return Ok(false); + }; let declared_max = self.kind.size_max.and_then(|max| usize::try_from(max).ok()).unwrap_or(usize::MAX); let max = declared_max.min(MAX_TABLE_SIZE); if len > max { - return Err(crate::Trap::TableOutOfBounds { offset: len, len: 1, max: self.elements.len() }); + return Ok(false); + } + if len == self.elements.len() { + return Ok(true); + } + if let Some(limiter) = limiter + && !limiter.table_growing(self.elements.len(), len, Self::maximum_size(self.kind))? + { + return Ok(false); } - cold_err!(self.elements.try_reserve_exact(n)).map_err(|_| Trap::OutOfMemory)?; + if cold_err!(self.elements.try_reserve_exact(n)).is_err() { + return Ok(false); + } self.elements.resize(len, init); - Ok(()) + Ok(true) + } + + fn maximum_size(kind: TableType) -> Option { + kind.size_max.and_then(|maximum| usize::try_from(maximum).ok()) } pub(crate) fn size(&self) -> usize { diff --git a/crates/tinywasm/tests/internal_refs.rs b/crates/tinywasm/tests/internal_refs.rs index 07cbde2f..1793e05f 100644 --- a/crates/tinywasm/tests/internal_refs.rs +++ b/crates/tinywasm/tests/internal_refs.rs @@ -65,7 +65,7 @@ fn exported_tables_and_globals_have_handle_and_helper_apis() -> Result<(), Box = Result>; @@ -51,6 +51,10 @@ impl ResourceLimiter for DenyAll { fn memory_growing(&self, _current: usize, _desired: usize, _maximum: Option) -> Result { Ok(false) } + + fn table_growing(&self, _current: usize, _desired: usize, _maximum: Option) -> Result { + Ok(false) + } } struct DenyGrowth; @@ -59,6 +63,10 @@ impl ResourceLimiter for DenyGrowth { fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { Ok(current == 0) } + + fn table_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { + Ok(current == 0) + } } struct TrapGrowth; @@ -67,6 +75,10 @@ impl ResourceLimiter for TrapGrowth { fn memory_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { if current == 0 { Ok(true) } else { Err(Trap::Unreachable) } } + + fn table_growing(&self, current: usize, _desired: usize, _maximum: Option) -> Result { + if current == 0 { Ok(true) } else { Err(Trap::Unreachable) } + } } #[test] @@ -138,6 +150,33 @@ fn resource_limiter_rejects_module_memory_initial_size() -> TestResult { Ok(()) } +#[test] +fn resource_limiter_rejects_table_initial_size() { + let mut store = store_with_limiter(Arc::new(DenyAll)); + let result = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into()); + + assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory)))); +} + +#[test] +fn resource_limiter_can_reject_table_growth() -> TestResult { + let mut store = store_with_limiter(Arc::new(DenyGrowth)); + let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?; + + assert_eq!(table.grow(&mut store, 1, RefValue::Null.into())?, None); + assert_eq!(table.size(&store)?, 1); + Ok(()) +} + +#[test] +fn resource_limiter_can_trap_table_growth() -> TestResult { + let mut store = store_with_limiter(Arc::new(TrapGrowth)); + let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?; + + assert!(matches!(table.grow(&mut store, 1, RefValue::Null.into()), Err(tinywasm::Error::Trap(Trap::Unreachable)))); + Ok(()) +} + #[test] fn resource_limiter_allows_guest_memory_grow_by_default() -> TestResult { let wasm = wat::parse_str(