From 7fd84b9654de0467c909d991b848334a328cd2a5 Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Mon, 31 Aug 2026 10:36:43 -0700 Subject: [PATCH 1/7] Clean up code-base from external review --- .github/workflows/ci.yml | 36 +++--- .github/workflows/comment.yml | 8 +- .github/workflows/welcome.yml | 2 +- Cargo.toml | 2 +- Makefile | 11 +- README.md | 2 +- crates/fuzzing/Cargo.toml | 2 +- crates/fuzzing/src/generators.rs | 11 -- crates/fuzzing/src/oracles.rs | 17 ++- crates/spacewasi/scripts/wasm2mvp.sh | 14 +-- crates/spacewasi/src/main.rs | 42 ++++--- crates/spacewasi/tests/wasm/fs.c | 4 + crates/spacewasm_c_api/CMakeLists.txt | 13 +- crates/spacewasm_c_api/include/spacewasm.h | 29 ++--- crates/spacewasm_c_api/src/alloc.rs | 5 +- crates/spacewasm_c_api/src/capi.rs | 17 ++- crates/spacewasm_c_api/src/host.rs | 1 - crates/spacewasm_c_api/src/status.rs | 16 +-- crates/spacewasm_c_api/src/tests.rs | 112 ++++++++++++++++-- crates/spacewasm_c_example/examples/ctest.c | 2 +- .../examples/ctest_suite.c | 26 +++- crates/spacewasm_std/benches/coremark.rs | 1 + crates/spacewasm_std/src/main.rs | 5 - .../spacewasm_util/src/bin/spacewasm-trace.rs | 7 +- crates/spacewasm_util/src/lib.rs | 7 +- crates/spacewasm_util/src/trace.rs | 2 + docs/REQUIREMENTS.md | 4 +- docs/ir.md | 10 +- fuzz/Cargo.toml | 3 - src/code.rs | 13 +- src/compiler.rs | 3 + src/host.rs | 56 ++++++--- src/opcode.rs | 30 ++--- src/types.rs | 2 +- src/util/vec.rs | 19 +-- tests/core/br_table.wast | 2 +- .../host_func_invalid_should_panic_none.wast | 5 +- .../host_func_invalid_should_panic_some.wast | 5 +- tests/util/spectest.rs | 32 +---- 39 files changed, 365 insertions(+), 213 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44e1c62..e1130ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c @@ -127,7 +127,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install 32-bit system libraries run: | @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Rust toolchain # Pinned to the commit the `stable` branch resolved to (its action.yml @@ -210,7 +210,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c @@ -228,7 +228,7 @@ jobs: contents: read steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 @@ -262,7 +262,7 @@ jobs: echo "$output" # Extract the CoreMark score from the output - score=$(echo "$output" | grep "CoreMark Score:" | awk '{print $3}') + score=$(echo "$output" | grep -m1 "CoreMark Score:" | awk '{print $3}') if [ -z "$score" ]; then echo "Failed to extract CoreMark score" exit 1 @@ -280,7 +280,7 @@ jobs: git fetch origin main:main git checkout main baseline_output=$(cargo bench -p spacewasm_std --bench coremark --no-fail-fast 2>&1) || true - baseline_score=$(echo "$baseline_output" | grep "CoreMark Score:" | awk '{print $3}') + baseline_score=$(echo "$baseline_output" | grep -m1 "CoreMark Score:" | awk '{print $3}') git checkout - if [ -n "$baseline_score" ]; then @@ -306,7 +306,7 @@ jobs: - name: Upload benchmark results if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: benchmark-results path: benchmark-results.json @@ -318,12 +318,12 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 - name: Run Kani on repo - uses: model-checking/kani-github-action@v1.1 + uses: model-checking/kani-github-action@f838096619a707b0f6b2118cf435eaccfa33e51f # v1.1 with: args: "-j --output-format=terse" @@ -332,16 +332,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c with: - profile: minimal toolchain: nightly - override: true components: miri - name: Run Miri @@ -354,7 +352,7 @@ jobs: contents: read steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 @@ -376,7 +374,9 @@ jobs: sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov + uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 + with: + tool: cargo-llvm-cov - name: Cache Rust dependencies uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 @@ -444,14 +444,14 @@ jobs: - name: Upload coverage results if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: coverage-results path: coverage-results.json - name: Upload coverage to Codecov if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml index 82548f4..67f3a15 100644 --- a/.github/workflows/comment.yml +++ b/.github/workflows/comment.yml @@ -17,7 +17,7 @@ jobs: if: github.event.workflow_run.event == 'pull_request' steps: - name: Download CI artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -27,7 +27,7 @@ jobs: continue-on-error: true - name: Post benchmark and coverage comments - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: TRUSTED_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} with: @@ -99,7 +99,7 @@ jobs: let comment = '## CoreMark Benchmark Results\n\n'; comment += `**Current Score:** ${currentScore.toFixed(3)}\n`; - if (baselineScore) { + if (baselineScore !== null) { const diff = currentScore - baselineScore; const percentChange = ((diff / baselineScore) * 100).toFixed(2); @@ -128,7 +128,7 @@ jobs: let comment = '## Code Coverage Report\n\n'; comment += `**Current Coverage:** ${currentCoverage.toFixed(2)}%\n`; - if (baselineCoverage) { + if (baselineCoverage !== null) { const diff = currentCoverage - baselineCoverage; comment += `**Baseline Coverage (main):** ${baselineCoverage.toFixed(2)}%\n`; diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 1a3428b..4c057aa 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -69,5 +69,5 @@ jobs: repo: context.repo.repo, body: `**Welcome**, new contributor! - Please make sure you've read our [contributing guide](https://github.com/nasa/spacewasm/blob/main/CONTRIBUTING.md), as well as our [policy regarding AI usage](https://github.com/nasa/spacewasm/blob/main/AI_POLICY.md), and we look forward to reviewing your pull request shortly` +Please make sure you've read our [contributing guide](https://github.com/nasa/spacewasm/blob/main/CONTRIBUTING.md), as well as our [policy regarding AI usage](https://github.com/nasa/spacewasm/blob/main/AI_POLICY.md), and we look forward to reviewing your pull request shortly` }) diff --git a/Cargo.toml b/Cargo.toml index 7588bd7..d25d7f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ default = [] strict-assertions = [] [dependencies] -libm = "0.2.16" +libm = "=0.2.16" [dev-dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/Makefile b/Makefile index 9b133d1..8ca7e84 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help fuzz fuzz-validate fuzz-validate-differential fuzz-malformed seed-to-wasm trace-wasm trace trace-debug clean-artifacts +.PHONY: help fuzz fuzz-validate fuzz-validate-differential fuzz-malformed seed-to-wasm trace-wasm trace-wasm-debug trace trace-debug clean-artifacts gen-header # Target Configuration (auto-detect if not set) SPACEWASM_TARGET ?= $(shell rustc -vV | grep 'host:' | cut -d' ' -f2) @@ -28,6 +28,9 @@ help: @echo " make trace-debug CRASH=crash-abc123 LIMIT=100" @echo " make trace-wasm WASM=output.wasm LIMIT=50" @echo "" + @echo "C API:" + @echo " make gen-header Regenerate crates/spacewasm_c_api/include/spacewasm.h with cbindgen" + @echo "" @echo "Utilities:" @echo " make clean-artifacts Delete all fuzzer artifacts" @@ -97,6 +100,12 @@ trace-wasm-debug: fi RUSTFLAGS="-Zsanitizer=address" cargo run --target $(SPACEWASM_TARGET) -p spacewasm_util --bin spacewasm-trace -- $(WASM) --limit $(or $(LIMIT),200) +# Regenerate the C API header from the Rust source with cbindgen. The header is +# written as a side effect of building spacewasm_c_api with the `codegen` feature. +gen-header: + cargo build -p spacewasm_c_api --features codegen + @echo "Regenerated crates/spacewasm_c_api/include/spacewasm.h" + # Clean fuzzer artifacts clean-artifacts: rm -rf fuzz/artifacts/* diff --git a/README.md b/README.md index d460208..67f1652 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

SpaceWasm

+

SpaceWasm

diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml index da8dbfe..eb79a8a 100644 --- a/crates/fuzzing/Cargo.toml +++ b/crates/fuzzing/Cargo.toml @@ -11,7 +11,7 @@ log = "0.4" wasm-smith = "0.220.0" wasmprinter = "0.220.0" env_logger = "0.11" -wasmi = { version = "1", optional = true } +wasmi = { version = "~1.1.0", optional = true } [features] # Differential validator testing against wasmi as a reference implementation. diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index 90a80dd..a810aba 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -35,7 +35,6 @@ impl ModuleConfig { // Disable imports - SpaceWasm tests don't provide import environment min_imports: 0, max_imports: 0, - // Memory limits (SpaceWasm supports max 256 pages = 16MB) memory_max_size_required: false, max_memory32_bytes: 65536 * 32, // Wasm 1.0 MVP compliance - disable all post-MVP features @@ -68,16 +67,6 @@ impl ModuleConfig { pub fn generate(&self, u: &mut Unstructured<'_>) -> Result { wasm_smith::Module::new(self.config.clone(), u) } - - /// Get the underlying wasm-smith config. - pub fn smith_config(&self) -> &SmithConfig { - &self.config - } - - /// Get a mutable reference to the underlying wasm-smith config. - pub fn smith_config_mut(&mut self) -> &mut SmithConfig { - &mut self.config - } } impl Default for ModuleConfig { diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 97e7f4f..2cb031e 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -116,7 +116,11 @@ unsafe impl Allocator for SystemAllocator { if layout.size() > MAX_ALLOCATION_BYTES { return Err(AllocError::OutOfMemory); } - unsafe { Ok(std::alloc::alloc(layout)) } + let ptr = unsafe { std::alloc::alloc(layout) }; + if ptr.is_null() { + return Err(AllocError::AllocationFailed); + } + Ok(ptr) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { @@ -503,7 +507,16 @@ pub fn no_traps(wasm: &[u8]) { // These should never trap since the module was generated with disallow_traps for (wasm_ref, params) in exported_funcs { state.reset(); - state.invoke(wasm_ref, ¶ms).unwrap(); + match state.invoke(wasm_ref, ¶ms) { + Ok(()) => {} + // Wasm Smith cannot avoid deeply-recursive exports; a stack overflow + // here is not a bug, so skip this export. + Err(InvokeError::StackOverflow) => { + log::debug!("export hit a stack overflow during invocation"); + continue; + } + Err(e) => panic!("unexpected invoke error in no_traps module: {e:?}"), + } // Run the interpreter with limited instructions let interpreter = Interpreter; diff --git a/crates/spacewasi/scripts/wasm2mvp.sh b/crates/spacewasi/scripts/wasm2mvp.sh index 43efc2d..37dccd8 100755 --- a/crates/spacewasi/scripts/wasm2mvp.sh +++ b/crates/spacewasi/scripts/wasm2mvp.sh @@ -1,16 +1,14 @@ #!/usr/bin/env bash -if ! command -v wasm-opt --version >/dev/null 2>&1 +if ! command -v wasm-opt >/dev/null 2>&1 then echo "please install wasm-opt to use this command" exit 1 fi -if [ "$#" -ne 2 ]; then - if [ "$#" -ne 1 ]; then - echo "usage: $0 input.wasm [output.wasm]" - exit 1 - fi +if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + echo "usage: $0 input.wasm [output.wasm]" + exit 1 fi wasm-opt \ @@ -20,7 +18,7 @@ wasm-opt \ --llvm-nontrapping-fptoint-lowering \ --disable-multivalue \ --disable-simd \ - $1 \ - -o ${2:-$1} + "$1" \ + -o "${2:-$1}" exit $? \ No newline at end of file diff --git a/crates/spacewasi/src/main.rs b/crates/spacewasi/src/main.rs index 7b4d4f7..4b0f6cc 100644 --- a/crates/spacewasi/src/main.rs +++ b/crates/spacewasi/src/main.rs @@ -18,7 +18,7 @@ /// Portions of this file are derived from use spacewasm::{ CodeBuilder, CompilerOptions, Engine, ExportDesc, Interpreter, InterpreterResult, - InterpreterRunner, InvokeError, ModuleRef, PageAllocator, Ref, TrapReason, WasmRef, + InterpreterRunner, InvokeError, PageAllocator, Ref, TrapReason, WasmRef, }; mod wasi_preview1; use crate::wasi_preview1::make_wasi_preview1_module; @@ -178,7 +178,7 @@ fn main() { }; let mut file_stream = FileStream::new(file); - let Ok(module) = spacewasm::Module::new::( + let module = match spacewasm::Module::new::( "main", &mut file_stream, &mut engine.store, @@ -186,9 +186,12 @@ fn main() { spacewasm::Rc::new(RustSystemAllocator) .unwrap() .into_wasm_memory_allocator(), - ) else { - eprintln!("failed to parse WASM module"); - std::process::exit(1); + ) { + Ok(module) => module, + Err(error) => { + eprintln!("failed to parse WASM module: {error:?}"); + std::process::exit(1); + } }; // Append the module and run its start function (if any). The interpreter @@ -221,7 +224,12 @@ fn main() { let module: &spacewasm::Module = engine.store.modules().last().unwrap(); let fi = { - let f = module.exports.iter().find(|f| &f.name == "_start").unwrap(); + let Some(f) = module.exports.iter().find(|f| &f.name == "_start") else { + eprintln!( + "error: the provided wasm module does not correctly export a _start function" + ); + std::process::exit(1); + }; let ExportDesc::Func(fi) = f.desc else { eprintln!( "error: the provided wasm module does not correctly export a _start function" @@ -236,17 +244,17 @@ fn main() { std::process::exit(1); }; - engine - .invoke( - WasmRef { - module: ModuleRef(0), - index: fi, - }, - &[], - ) - .unwrap(); - - let mut result = InterpreterResult::OutOfFuel; + let mut result = match engine.invoke( + WasmRef { + module: module_ref, + index: fi, + }, + &[], + ) { + Ok(()) => InterpreterResult::OutOfFuel, + Err(InvokeError::StackOverflow) => InterpreterResult::Trap(TrapReason::StackOverflow), + Err(_) => unreachable!(), + }; while result == InterpreterResult::OutOfFuel { result = Interpreter.run(code_builder.pages(), &mut engine, usize::MAX) } diff --git a/crates/spacewasi/tests/wasm/fs.c b/crates/spacewasi/tests/wasm/fs.c index 5a57b2f..0edd9bc 100644 --- a/crates/spacewasi/tests/wasm/fs.c +++ b/crates/spacewasi/tests/wasm/fs.c @@ -4,6 +4,10 @@ int main() { FILE *f; f = fopen("dummyfile", "r"); + if (f == NULL) { + fprintf(stderr, "error: could not open dummyfile\n"); + return 1; + } char content[100]; fgets(content, 100, f); printf("%s", content); diff --git a/crates/spacewasm_c_api/CMakeLists.txt b/crates/spacewasm_c_api/CMakeLists.txt index c9afbe3..69a9755 100644 --- a/crates/spacewasm_c_api/CMakeLists.txt +++ b/crates/spacewasm_c_api/CMakeLists.txt @@ -5,9 +5,15 @@ include(ExternalProject) set(SPACEWASM_TARGET "" CACHE STRING "Target triple for Rust compiler") if(SPACEWASM_TARGET STREQUAL "") - execute_process(COMMAND rustc -vV OUTPUT_VARIABLE RUSTC_VERSION) - string(REGEX MATCH "host: ([^ \n]*)" RUSTC_HOST ${RUSTC_VERSION}) - string(STRIP ${CMAKE_MATCH_1} RUSTC_HOST_TARGET) + execute_process( + COMMAND rustc -vV + OUTPUT_VARIABLE RUSTC_VERSION + RESULT_VARIABLE RUSTC_RESULT) + if(NOT RUSTC_RESULT EQUAL 0) + message(FATAL_ERROR "rustc not found or failed to run; set SPACEWASM_TARGET to the desired target triple") + endif() + string(REGEX MATCH "host: ([^ \n]*)" RUSTC_HOST "${RUSTC_VERSION}") + string(STRIP "${CMAKE_MATCH_1}" RUSTC_HOST_TARGET) set(SPACEWASM_TARGET ${RUSTC_HOST_TARGET}) endif() @@ -40,6 +46,7 @@ ExternalProject_Add( BUILD_COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} ${SPACEWASM_CARGO_BINARY} build + --locked --target ${SPACEWASM_TARGET} --package spacewasm_c_api ${SPACEWASM_BUILD_TYPE_FLAG} diff --git a/crates/spacewasm_c_api/include/spacewasm.h b/crates/spacewasm_c_api/include/spacewasm.h index 3bb7d70..0d4da33 100644 --- a/crates/spacewasm_c_api/include/spacewasm.h +++ b/crates/spacewasm_c_api/include/spacewasm.h @@ -13,9 +13,6 @@ /* Operation status returned by most `spacewasm_*` functions. [`spacewasm_status_t::SPACEWASM_OK`] (0) means success. - - Variants are glob-re-exported below so they can be named unqualified within - this crate (e.g. `status::SPACEWASM_OK`). */ enum spacewasm_status_t #if defined(__cplusplus) || __STDC_VERSION__ >= 202311L @@ -340,6 +337,12 @@ typedef struct spacewasm_t spacewasm_t; typedef struct spacewasm_host_module_t spacewasm_host_module_t; +/* + Opaque handle passed to C host callbacks, wrapping a borrowed core + [`Engine`]. Valid only for the duration of the call. + */ +typedef struct spacewasm_caller_t spacewasm_caller_t; + /* Allocate `size` bytes aligned to `align`. Return NULL on failure. */ @@ -366,14 +369,6 @@ typedef struct spacewasm_host_t { uint32_t len; } spacewasm_host_t; -/* - Opaque handle passed to C host callbacks, wrapping a borrowed core - [`Engine`]. Valid only for the duration of the call. - */ -typedef struct spacewasm_caller_t { - -} spacewasm_caller_t; - /* FFI-safe union of the four WebAssembly 1.0 value payloads. */ @@ -488,10 +483,10 @@ struct spacewasm_allocator_t *spacewasm_allocator_new(spacewasm_alloc_fn_t alloc void spacewasm_allocator_destroy(struct spacewasm_allocator_t *allocator); /* - Create a new host module vector of max_host_module size + Create a new host module vector of `len` size # Safety - `host` must be live + `dest` must be null or a valid, live pointer to write the new host vector into. */ spacewasm_status_t spacewasm_host_new(uint32_t len, struct spacewasm_host_t *dest); @@ -635,9 +630,11 @@ spacewasm_status_t spacewasm_module_start(struct spacewasm_t *engine, parameter or return count differs, and [`spacewasm_status_t::SPACEWASM_ERR_PARAM_TYPE_MISMATCH`] when a type at some position differs. Returns [`spacewasm_status_t::SPACEWASM_ERR_NOT_FOUND`] - when `module_idx` or `func_index` is out of range, and - [`spacewasm_status_t::SPACEWASM_ERR_BAD_SIGNATURE`] when a signature string - contains a character other than `iIfd` or is too long. + when `module_idx` or `func_index` is out of range. When a signature string + contains a character other than `iIfd` it returns + [`spacewasm_status_t::SPACEWASM_ERR_BAD_ARG`], and when it declares more than + `MAX_HOST_FUNCTION_PARAMS` entries it returns + [`spacewasm_status_t::SPACEWASM_ERR_FUNCTION_PARAMETERS_TOO_LARGE`]. # Safety `engine` must be live; all C strings valid and NUL-terminated. diff --git a/crates/spacewasm_c_api/src/alloc.rs b/crates/spacewasm_c_api/src/alloc.rs index d13f7ed..0c1be02 100644 --- a/crates/spacewasm_c_api/src/alloc.rs +++ b/crates/spacewasm_c_api/src/alloc.rs @@ -109,8 +109,9 @@ pub unsafe fn allocator_clone_rc(handle: *const CAllocator) -> Option but the lifetime is not - // defined from the C side. + // The handle is a leaked `Rc` (a pointer owning a refcount). + // Wrapping the reconstructed `Rc` in `ManuallyDrop` lets us clone from it + // without decrementing the borrowed refcount when this binding drops. let handle: core::mem::ManuallyDrop> = unsafe { core::mem::transmute::<*const CAllocator, _>(handle) }; Some(Rc::clone(&handle).into_wasm_memory_allocator()) diff --git a/crates/spacewasm_c_api/src/capi.rs b/crates/spacewasm_c_api/src/capi.rs index 6c34267..fddba53 100644 --- a/crates/spacewasm_c_api/src/capi.rs +++ b/crates/spacewasm_c_api/src/capi.rs @@ -128,10 +128,10 @@ impl From<&mut spacewasm_host_t> for &mut Vec { } } -/// Create a new host module vector of max_host_module size +/// Create a new host module vector of `len` size /// /// # Safety -/// `host` must be live +/// `dest` must be null or a valid, live pointer to write the new host vector into. #[unsafe(no_mangle)] pub unsafe extern "C" fn spacewasm_host_new( len: u32, @@ -509,9 +509,11 @@ pub unsafe extern "C" fn spacewasm_module_start( /// parameter or return count differs, and /// [`spacewasm_status_t::SPACEWASM_ERR_PARAM_TYPE_MISMATCH`] when a type at some /// position differs. Returns [`spacewasm_status_t::SPACEWASM_ERR_NOT_FOUND`] -/// when `module_idx` or `func_index` is out of range, and -/// [`spacewasm_status_t::SPACEWASM_ERR_BAD_SIGNATURE`] when a signature string -/// contains a character other than `iIfd` or is too long. +/// when `module_idx` or `func_index` is out of range. When a signature string +/// contains a character other than `iIfd` it returns +/// [`spacewasm_status_t::SPACEWASM_ERR_BAD_ARG`], and when it declares more than +/// `MAX_HOST_FUNCTION_PARAMS` entries it returns +/// [`spacewasm_status_t::SPACEWASM_ERR_FUNCTION_PARAMETERS_TOO_LARGE`]. /// /// # Safety /// `engine` must be live; all C strings valid and NUL-terminated. @@ -782,6 +784,11 @@ pub unsafe extern "C" fn spacewasm_run( fuel: usize, out_trap: *mut spacewasm_trap_t, ) -> spacewasm_run_status_t { + // Clear out the destination before we do any early returns + if !out_trap.is_null() { + unsafe { *out_trap = spacewasm_trap_t::SPACEWASM_TRAP_NONE }; + } + let Some(cengine) = (unsafe { engine.as_mut() }) else { return spacewasm_run_status_t::SPACEWASM_RUN_TRAP; }; diff --git a/crates/spacewasm_c_api/src/host.rs b/crates/spacewasm_c_api/src/host.rs index 960b104..efd0cf0 100644 --- a/crates/spacewasm_c_api/src/host.rs +++ b/crates/spacewasm_c_api/src/host.rs @@ -13,7 +13,6 @@ const MAX_HOST_PARAMS: usize = 32; /// Opaque handle passed to C host callbacks, wrapping a borrowed core /// [`Engine`]. Valid only for the duration of the call. -#[repr(C)] pub struct SpacewasmCaller; impl SpacewasmCaller { diff --git a/crates/spacewasm_c_api/src/status.rs b/crates/spacewasm_c_api/src/status.rs index f4cc16f..097c648 100644 --- a/crates/spacewasm_c_api/src/status.rs +++ b/crates/spacewasm_c_api/src/status.rs @@ -2,15 +2,12 @@ //! stable integer ABI is owned by the FFI layer, not the core crate. use spacewasm::{ - AllocError, ConstantExprError, HostNameError, HostValListError, InterpreterResult, InvokeError, - MemoryError, ParseError, TrapReason, ValidationError, + AllocError, ConstantExprError, HostFunctionError, HostNameError, InterpreterResult, + InvokeError, MemoryError, ParseError, TrapReason, ValidationError, }; /// Operation status returned by most `spacewasm_*` functions. /// [`spacewasm_status_t::SPACEWASM_OK`] (0) means success. -/// -/// Variants are glob-re-exported below so they can be named unqualified within -/// this crate (e.g. `status::SPACEWASM_OK`). #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum spacewasm_status_t { @@ -362,8 +359,13 @@ pub fn host_name_status(_e: HostNameError) -> spacewasm_status_t { SPACEWASM_ERR_NAME_TOO_LONG } -pub fn host_val_list_status(_e: HostValListError) -> spacewasm_status_t { - SPACEWASM_ERR_BAD_SIGNATURE +pub fn host_val_list_status(e: HostFunctionError) -> spacewasm_status_t { + match e { + HostFunctionError::ValListInvalidItem => SPACEWASM_ERR_BAD_ARG, + HostFunctionError::ParameterListTooLong => SPACEWASM_ERR_FUNCTION_PARAMETERS_TOO_LARGE, + HostFunctionError::MultiReturnNotAllowed => SPACEWASM_ERR_FUNCTION_RETURNS_TOO_LARGE, + HostFunctionError::AllocError(ae) => alloc_status(ae), + } } /// Translate an [`InterpreterResult`] into a run status + trap code. diff --git a/crates/spacewasm_c_api/src/tests.rs b/crates/spacewasm_c_api/src/tests.rs index a552d07..356b3f4 100644 --- a/crates/spacewasm_c_api/src/tests.rs +++ b/crates/spacewasm_c_api/src/tests.rs @@ -560,10 +560,10 @@ fn check_func_signature() { "wrong return type" ); - // Malformed signature string. + // Malformed signature string: an invalid value-list character. assert_eq!( spacewasm_check_func_signature(store, idx, func, c"ix".as_ptr(), c"i".as_ptr()), - status::SPACEWASM_ERR_BAD_SIGNATURE, + status::SPACEWASM_ERR_BAD_ARG, "bad signature char" ); @@ -1362,7 +1362,7 @@ fn error_paths() { "oversized max_modules" ); - // Bad signature char -> ERR_BAD_SIGNATURE, no panic. + // Bad signature char -> ERR_BAD_ARG, no panic. assert_eq!( unsafe { spacewasm_host_new(1, host.as_mut_ptr()) }, status::SPACEWASM_OK @@ -1383,7 +1383,7 @@ fn error_paths() { Some(add_one), core::ptr::null_mut(), ), - status::SPACEWASM_ERR_BAD_SIGNATURE, + status::SPACEWASM_ERR_BAD_ARG, "bad signature" ); spacewasm_host_destroy(host.as_mut_ptr()); @@ -1882,7 +1882,7 @@ fn invoke_status_maps() { #[test] fn simple_error_mappers() { - use spacewasm::{HostNameError, HostValListError, SectionDecodeError, ValidationError}; + use spacewasm::{HostNameError, HostFunctionError, SectionDecodeError, ValidationError}; let pe = spacewasm::ParseError::new(0, SectionDecodeError::new(ValidationError::Eof)); assert_eq!(status::parse_status(&pe), status::SPACEWASM_ERR_EOF); @@ -1891,10 +1891,35 @@ fn simple_error_mappers() { status::host_name_status(HostNameError), status::SPACEWASM_ERR_NAME_TOO_LONG ); + + // Every HostFunctionError variant maps to a distinct, stable status code. + assert_eq!( + status::host_val_list_status(HostFunctionError::ValListInvalidItem), + status::SPACEWASM_ERR_BAD_ARG, + "invalid value-list character" + ); assert_eq!( - status::host_val_list_status(HostValListError), - status::SPACEWASM_ERR_BAD_SIGNATURE + status::host_val_list_status(HostFunctionError::ParameterListTooLong), + status::SPACEWASM_ERR_FUNCTION_PARAMETERS_TOO_LARGE, + "too many parameters" ); + assert_eq!( + status::host_val_list_status(HostFunctionError::MultiReturnNotAllowed), + status::SPACEWASM_ERR_FUNCTION_RETURNS_TOO_LARGE, + "multiple return values" + ); + // The AllocError variant forwards to the shared allocator mapping. + for ae in [ + spacewasm::AllocError::AllocationFailed, + spacewasm::AllocError::OutOfMemory, + spacewasm::AllocError::PageTooSmall, + ] { + assert_eq!( + status::host_val_list_status(HostFunctionError::AllocError(ae.clone())), + status::alloc_status(ae), + "alloc error forwards to alloc_status" + ); + } } #[test] @@ -2067,7 +2092,7 @@ fn module_with_start_runs() { status::SPACEWASM_OK ); - // Drive the start function in small fuel slices to also exercise the + // Drive the start function to completion. let mut trap = spacewasm_trap_t::SPACEWASM_TRAP_NONE; let start_status = run_to_completion(store, &mut trap); assert_eq!( @@ -2097,7 +2122,6 @@ fn module_with_start_runs() { ); assert_eq!(unsafe { out.u.i32_ }, 42, "start wrote 42"); - // A second module with no start reports `needs_start == false`. unsafe { spacewasm_destroy(store); spacewasm_allocator_destroy(alloc); @@ -2577,6 +2601,76 @@ fn add_host_function_not_found_module() { } } +#[test] +fn add_host_function_signature_errors() { + let _guard = ALLOC_LOCK.lock().unwrap(); + ensure_global_allocator(); + + let mut host = core::mem::MaybeUninit::::uninit(); + assert_eq!( + unsafe { spacewasm_host_new(1, host.as_mut_ptr()) }, + status::SPACEWASM_OK + ); + let mut hmod = 0u32; + unsafe { + assert_eq!( + spacewasm_add_host_module(host.as_mut_ptr(), c"env".as_ptr(), 4, 0, &mut hmod), + status::SPACEWASM_OK + ); + + // Helper to register a function with the given signatures, mapping each + // distinct HostFunctionError variant to its FFI status code. + let mut add = |name: &core::ffi::CStr, params: &core::ffi::CStr, returns: &core::ffi::CStr| { + spacewasm_add_host_function( + host.as_mut_ptr(), + hmod, + name.as_ptr(), + params.as_ptr(), + returns.as_ptr(), + Some(add_one), + core::ptr::null_mut(), + ) + }; + + // Invalid value-list character in the parameter signature. + assert_eq!( + add(c"bad_param", c"x", c""), + status::SPACEWASM_ERR_BAD_ARG, + "invalid param char -> ValListInvalidItem" + ); + + // Invalid value-list character in the return signature. + assert_eq!( + add(c"bad_ret", c"i", c"z"), + status::SPACEWASM_ERR_BAD_ARG, + "invalid return char -> ValListInvalidItem" + ); + + // More than MAX_HOST_FUNCTION_PARAMS (9) parameters. + assert_eq!( + add(c"too_many", c"iiiiiiiiii", c""), + status::SPACEWASM_ERR_FUNCTION_PARAMETERS_TOO_LARGE, + "10 params -> ParameterListTooLong" + ); + + // More than one return value is not supported. + assert_eq!( + add(c"multi_ret", c"i", c"ii"), + status::SPACEWASM_ERR_FUNCTION_RETURNS_TOO_LARGE, + "two returns -> MultiReturnNotAllowed" + ); + + // A valid signature still succeeds after the rejected attempts. + assert_eq!( + add(c"ok", c"i", c"i"), + status::SPACEWASM_OK, + "valid signature registers" + ); + + spacewasm_host_destroy(host.as_mut_ptr()); + } +} + #[test] fn allocator_new_rejects_null_callbacks() { // Any null callback yields a null handle (no allocation performed). diff --git a/crates/spacewasm_c_example/examples/ctest.c b/crates/spacewasm_c_example/examples/ctest.c index f29c886..1d6bab5 100644 --- a/crates/spacewasm_c_example/examples/ctest.c +++ b/crates/spacewasm_c_example/examples/ctest.c @@ -204,7 +204,7 @@ int main(void) { spacewasm_trap_t trap = SPACEWASM_TRAP_NONE; spacewasm_run_status_t rs = SPACEWASM_RUN_OUT_OF_FUEL; while (rs == SPACEWASM_RUN_OUT_OF_FUEL) { - rs = spacewasm_run(store, 1000, &start_trap); + rs = spacewasm_run(store, 1000, &trap); } if (rs != SPACEWASM_RUN_FINISHED) { diff --git a/crates/spacewasm_c_example/examples/ctest_suite.c b/crates/spacewasm_c_example/examples/ctest_suite.c index a1efc51..11a56d7 100644 --- a/crates/spacewasm_c_example/examples/ctest_suite.c +++ b/crates/spacewasm_c_example/examples/ctest_suite.c @@ -673,13 +673,29 @@ static int test_error_paths(void) { CHECK(spacewasm_new(&host, 1024, 257, opts(256), &store) == SPACEWASM_ERR_BAD_ARG, "oversized max_modules"); - /* Bad signature char -> ERR_BAD_SIGNATURE, no panic. */ + /* Host function signature errors each map to a distinct status, no panic. */ CHECK(spacewasm_host_new(1, &host) == SPACEWASM_OK, "host_new"); uint32_t hmod = 0; - CHECK(spacewasm_add_host_module(&host, "env", 1, 0, &hmod) == SPACEWASM_OK, "add_host_module"); - CHECK(spacewasm_add_host_function(&host, hmod, "bad", "x", "", add_one, NULL) == - SPACEWASM_ERR_BAD_SIGNATURE, - "bad signature"); + CHECK(spacewasm_add_host_module(&host, "env", 4, 0, &hmod) == SPACEWASM_OK, "add_host_module"); + /* Invalid value-list character in the parameter signature. */ + CHECK(spacewasm_add_host_function(&host, hmod, "bad_param", "x", "", add_one, NULL) == + SPACEWASM_ERR_BAD_ARG, + "invalid param char"); + /* Invalid value-list character in the return signature. */ + CHECK(spacewasm_add_host_function(&host, hmod, "bad_ret", "i", "z", add_one, NULL) == + SPACEWASM_ERR_BAD_ARG, + "invalid return char"); + /* More than MAX_HOST_FUNCTION_PARAMS (9) parameters. */ + CHECK(spacewasm_add_host_function(&host, hmod, "too_many", "iiiiiiiiii", "", add_one, NULL) == + SPACEWASM_ERR_FUNCTION_PARAMETERS_TOO_LARGE, + "too many params"); + /* More than one return value is not supported. */ + CHECK(spacewasm_add_host_function(&host, hmod, "multi_ret", "i", "ii", add_one, NULL) == + SPACEWASM_ERR_FUNCTION_RETURNS_TOO_LARGE, + "multiple returns"); + /* A valid signature still succeeds after the rejected attempts. */ + CHECK(spacewasm_add_host_function(&host, hmod, "ok", "i", "i", add_one, NULL) == SPACEWASM_OK, + "valid signature"); spacewasm_host_destroy(&host); /* Malformed wasm -> parse error; the store is still created fine. */ diff --git a/crates/spacewasm_std/benches/coremark.rs b/crates/spacewasm_std/benches/coremark.rs index ee0a30b..22a3bca 100644 --- a/crates/spacewasm_std/benches/coremark.rs +++ b/crates/spacewasm_std/benches/coremark.rs @@ -35,6 +35,7 @@ fn main() { "".into(), "I".into(), |_, _| { + CLOCK_CALL_COUNT.fetch_add(1, Ordering::Relaxed); let ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() diff --git a/crates/spacewasm_std/src/main.rs b/crates/spacewasm_std/src/main.rs index d3a7ef2..9a42374 100644 --- a/crates/spacewasm_std/src/main.rs +++ b/crates/spacewasm_std/src/main.rs @@ -237,11 +237,6 @@ fn main() { ) .unwrap(); - // let dbg = Inspector { - // v: &interpreter, - // out: *out, - // }; - let mut result = InterpreterResult::OutOfFuel; while result == InterpreterResult::OutOfFuel { result = spacewasm::Interpreter.run(text, &mut state, usize::MAX) diff --git a/crates/spacewasm_util/src/bin/spacewasm-trace.rs b/crates/spacewasm_util/src/bin/spacewasm-trace.rs index 4d87f97..8b969c9 100644 --- a/crates/spacewasm_util/src/bin/spacewasm-trace.rs +++ b/crates/spacewasm_util/src/bin/spacewasm-trace.rs @@ -66,7 +66,12 @@ spacewasm::global_allocator!(SystemAllocator, SystemAllocator); unsafe impl Allocator for SystemAllocator { unsafe fn alloc(&self, layout: std::alloc::Layout) -> Result<*mut u8, AllocError> { - unsafe { Ok(std::alloc::alloc(layout)) } + let ptr = unsafe { std::alloc::alloc(layout) }; + if ptr.is_null() { + Err(AllocError::AllocationFailed) + } else { + Ok(ptr) + } } unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) { diff --git a/crates/spacewasm_util/src/lib.rs b/crates/spacewasm_util/src/lib.rs index 0a9072c..21a83ce 100644 --- a/crates/spacewasm_util/src/lib.rs +++ b/crates/spacewasm_util/src/lib.rs @@ -12,7 +12,12 @@ pub use trace::*; pub struct RustSystemAllocator; unsafe impl Allocator for RustSystemAllocator { unsafe fn alloc(&self, layout: Layout) -> Result<*mut u8, AllocError> { - unsafe { Ok(std::alloc::alloc(layout)) } + let ptr = unsafe { std::alloc::alloc(layout) }; + if ptr.is_null() { + Err(AllocError::AllocationFailed) + } else { + Ok(ptr) + } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { diff --git a/crates/spacewasm_util/src/trace.rs b/crates/spacewasm_util/src/trace.rs index 13dbca2..b005b77 100644 --- a/crates/spacewasm_util/src/trace.rs +++ b/crates/spacewasm_util/src/trace.rs @@ -23,6 +23,8 @@ pub struct StateHistory { impl StateHistory { pub fn new(capacity: usize) -> Self { + // Capacity must be at least 1 + let capacity = capacity.max(1); Self { snapshots: std::vec::Vec::with_capacity(capacity), capacity, diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 5ad2885..264c0aa 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -8,14 +8,14 @@ Adapted from [DLR-FT's Wasm interpreter](https://raw.githubusercontent.com/DLR-F | REQ-2 | No native/host stack | The interpreter shall not use the native/host stack to implement interpreted function calls. | The interpreted stack lives in a traditional data structure (e.g. `Vec`) rather than host function calls, so interpretation can be interrupted between any two virtual instructions and resumed from there. | | REQ-3 | Baremetal | The interpreter shall be executable on bare-metal environments. | No reliance on any specific functionality from the execution environment, so the interpreter can be embedded in any environment the language can compile for. | | REQ-4 | No implicit heap | The interpreter shall not rely on a default panicking allocator; any allocator must return a permissive (non-panicking) result for failed allocations. | A stock `alloc` panics on allocation failure, which is unacceptable on bare environments without an operating system. | -| REQ-5 | Configurable bounded execution | The interpreter shall yield back control flow eventually, within a configurable bound, and support fuel-bounded execution. | A user shall be able to call the interpreter with a bound and expect a result in a finite number of bytecode operations, even for non-terminating bytecode. Fuel is the mechanism: each instruction consumes a fixed amount of fuel and the interpreter yields when fuel is insufficient, resuming later. | +| REQ-5 | Configurable bounded execution | The interpreter shall yield back control flow eventually, within a configurable bound, and support fuel-bounded execution. | A user shall be able to call the interpreter with a bound and expect a result in a finite number of bytecode operations, even for non-terminating bytecode. Fuel is the mechanism: each instruction consumes a fixed amount of fuel and the interpreter yields when fuel is insufficient, resuming later. The bound is expressed in instructions, not wall-clock time; a single unit of fuel does not bound per-instruction execution time. | | REQ-6 | Instrumentation | The interpreter shall implement means for instrumentation. | Instrumentation generates evidence for certification, eases debugging, and enables run-time monitoring. | | REQ-7 | Migrateable | The interpreter state shall be able to halt on one computer and continue execution on another. | Enables load-balancing and redundancy: running applications migrate between computers without disruption. | | REQ-8 | De-/Serializable | The interpreter state shall be de-/serializable to and from a canonical representation. | A canonical serializable state makes migration and check-point/lock-step execution simple. | | REQ-9 | No Dependencies | No dependencies but those explicitly allowed by a requirement shall be used. | Dependencies must undergo the full V&V process, which adds enormous work. Exceptions must reference this requirement as parent and pin an exact version (major.minor.patch). | | REQ-10 | Fixed, cappable memory | The interpreter shall operate on a fixed memory and be able to cap it. | Bounding memory is required for deterministic behavior on constrained targets. | | REQ-11 | Bounded dynamic allocation | Dynamic allocation shall occur exclusively during module initialization, be limited to a maximum set of fixed-size pages, grow strictly during init, remain constant during execution, and be freed at completion. | Confining allocation to init with a fixed page cap eliminates fragmentation and keeps execution-time memory constant. | -| REQ-12 | Allow `libm` dependency | The interpreter may depend on `libm` `0.2.8`, provided it can be built from source. | `no_std` targets don't expose all floating-point ops (`abs`, `ceil`, `floor`, `trunc`, `round`, `sqrt`, `copysign`) without a C `libm`. | +| REQ-12 | Allow `libm` dependency | The interpreter may depend on `libm` `0.2.16`, provided it can be built from source. | `no_std` targets don't expose all floating-point ops (`abs`, `ceil`, `floor`, `trunc`, `round`, `sqrt`, `copysign`) without a C `libm`. | | REQ-13 | Target Wasm specification | The interpreter shall target Wasm 1.0 Recommendation (MVP + mutable globals), with select post-MVP proposals adopted to support `wasm32-unknown-unknown` rather than `wasm32v1-none`. | Smaller binaries and faster execution for low complexity overhead. | | REQ-14 | Decoding / validation bounds | Decoding and validation shall be bounded and able to halt, consistent with the execution bound. | Decoding and validation must not run unbounded, especially under streaming. | | REQ-15 | Streaming / chunked decode | The interpreter shall support providing the `.wasm` binary in chunks, processing each chunk as it arrives. | Avoids allocating one contiguous region large enough for the entire binary. Pairs with a 16-bit-aligned IR held in pages that point to each other, boosting execution speed; the raw Wasm binary need not be retained. | diff --git a/docs/ir.md b/docs/ir.md index 5fed660..2ba1825 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -59,7 +59,7 @@ Wasm instructions represented in the IR fall in the following categories: [16:operand_[0-15]] [16:operand_[16-31]] [16:operand_[32-47]] - [16:operand_[48-64]] + [16:operand_[48-63]] ``` 6. Local Operand ``` @@ -75,12 +75,12 @@ Wasm instructions represented in the IR fall in the following categories: ## Limitations -Are several limitations enforced by the interpreter that generally stem +There are several limitations enforced by the interpreter that generally stem from tuned fixed-width integer constraints imposed by the IR design and datastructures within the implementation. > [!NOTE] -> We have found that these limitations are generally not highly restrictive and even got a Pyiodide interpreter running inside SpaceWasm. +> We have found that these limitations are generally not highly restrictive and even got a Pyodide interpreter running inside SpaceWasm. ### Module & Store Limits @@ -94,14 +94,14 @@ datastructures within the implementation. These follow the WebAssembly 1.0 specification and are validated at decode time. - **Page size**: 64 KiB or 1 B — [custom-page-sizes proposal](https://github.com/WebAssembly/custom-page-sizes) is implemented. -- **Maximum pages**: 4 GiB per memory. A declared `min` or `max` above this is rejected. +- **Maximum memory size**: 4 GiB per memory. A declared `min` or `max` above this is rejected. ### IR Code Pages These pages hold the compiled IR (not raw Wasm bytecode) and are distinct from linear-memory pages. This limit comes from the encoding of program counters and the design choices of the IR. -- **Code pages**: Configurable via generic parameter `MAX_CODE_PAGES`, typically set at module instantiation +- **Code pages**: Configurable via the runtime option `max_code_pages`, set at module instantiation and validated against the 24-bit page-index bound - **Page size**: 256 16-bit words (512 bytes) - **Maximum page index**: 24-bit (16,777,216 pages) - **Word offset in page**: 8-bit (0-255) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 11d916f..88c12ad 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -42,6 +42,3 @@ bench = false [profile.release] debug = 1 - -# Optional: Add this section if you want to disable sanitizers for specific targets -# This requires setting up custom build profiles diff --git a/src/code.rs b/src/code.rs index 24312e8..f570eb2 100644 --- a/src/code.rs +++ b/src/code.rs @@ -108,11 +108,14 @@ impl Module { })?; // Compute the local size in words - let size_in_words = f - .locals - .iter() - .fold(0, |sum, (n, ty)| sum + (*n as usize) * ty.size()) - / 4; + let mut total_size: usize = 0; + for (n, ty) in f.locals.iter() { + total_size = (*n as usize) + .checked_mul(ty.size()) + .and_then(|v| total_size.checked_add(v)) + .ok_or(ValidationError::TooManyLocals)?; + } + let size_in_words = total_size / 4; if size_in_words > 0xFFFF { return Err(ValidationError::TooManyLocals); diff --git a/src/compiler.rs b/src/compiler.rs index 5bdfcdd..c63f8ae 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -359,6 +359,9 @@ impl<'a, const MAX_CONTROL_FRAMES: usize, const MAX_STACK_DEPTH: usize> WasmVisi } } + // The four `*_reinterpret_*` handlers intentionally emit no IR instruction: + // a reinterpret is a bit-level no-op on the raw operand stack, so only the + // stack types need validating. The absence of an emitted opcode is deliberate. fn i32_reinterpret_f32(&self, state: &mut Self::State) -> Result<(), Self::Error> { validate!(state, (F32) -> (I32)); Ok(()) diff --git a/src/host.rs b/src/host.rs index 0b02630..ab677cc 100644 --- a/src/host.rs +++ b/src/host.rs @@ -167,10 +167,27 @@ pub type HostFunctionResult = ControlFlow>; /// Maximum number of parameters a host function may declare. pub const MAX_HOST_FUNCTION_PARAMS: usize = 9; -/// Error returned when a host value signature contains an invalid character or -/// exceeds [`HOST_SIGNATURE_CAP`] entries. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct HostValListError; +/// Error returned when processing a host function item +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HostFunctionError { + /// Each HostValList item must be i, I, f, or d + ValListInvalidItem, + + /// Host function signature is longer than MAX_HOST_FUNCTION_PARAMS + ParameterListTooLong, + + /// Multiple return values are not supported + MultiReturnNotAllowed, + + /// Could not box host function closure + AllocError(AllocError), +} + +impl From for HostFunctionError { + fn from(value: AllocError) -> Self { + HostFunctionError::AllocError(value) + } +} /// An owned, bounded list of [`ValType`] describing a host function's /// parameters or results. Parsed from a signature string of the characters @@ -194,13 +211,13 @@ impl From for ResultType { } impl HostValList { - fn map_char(c: char) -> Result { + fn map_char(c: char) -> Result { match c { 'i' => Ok(ValType::I32), 'I' => Ok(ValType::I64), 'f' => Ok(ValType::F32), 'd' => Ok(ValType::F64), - _ => Err(HostValListError), + _ => Err(HostFunctionError::ValListInvalidItem), } } @@ -214,13 +231,13 @@ impl HostValList { /// Fallibly construct a signature list. Returns an error if any character /// is not one of `iIfd` or the signature exceeds [`MAX_HOST_FUNCTION_PARAMS`] entries. /// This is the FFI-safe constructor. - pub fn try_new(s: &str) -> Result { + pub fn try_new(s: &str) -> Result { let mut data = [ValType::I32; MAX_HOST_FUNCTION_PARAMS]; let mut len = 0usize; for c in s.chars() { if len >= MAX_HOST_FUNCTION_PARAMS { - return Err(HostValListError); + return Err(HostFunctionError::ParameterListTooLong); } data[len] = HostValList::map_char(c)?; len += 1; @@ -370,20 +387,27 @@ impl HostFunction { params: HostValList, returns: HostValList, f: impl Fn(&mut Engine, &[Value]) -> HostFunctionResult + 'static, - ) -> Result { - let ps = params.iter().fold(0, |n, i| n + i.size()) / 4; - if ps > 0xFFFF { - return Err(HostValListError); + ) -> Result { + if params.len() > MAX_HOST_FUNCTION_PARAMS { + return Err(HostFunctionError::ParameterListTooLong); } - if params.len() > MAX_HOST_FUNCTION_PARAMS { - return Err(HostValListError); + // A sanity check to make sure that the parameters fit within the IR supported + // frame size. This check is unwrapped because the `MAX_HOST_FUNCTION_PARAMS` should + // already guard against this check (i.e. the parameter length also bounds the param size). + let ps = params + .iter() + .fold(0, |n: usize, i| n.checked_add(i.size()).unwrap()) + / 4; + + if ps > 0xFFFF { + unreachable!() } let mut rs: Option = None; for r in returns.iter() { if rs.is_some() { - return Err(HostValListError); + return Err(HostFunctionError::MultiReturnNotAllowed); } rs = Some(r); @@ -393,7 +417,7 @@ impl HostFunction { name, params, returns: ResultType(rs), - f: Box::new(f).unwrap().into_host_function_dyn(), + f: Box::new(f)?.into_host_function_dyn(), }) } diff --git a/src/opcode.rs b/src/opcode.rs index 5f8246b..6038bd7 100644 --- a/src/opcode.rs +++ b/src/opcode.rs @@ -1,18 +1,18 @@ -/// All opcodes, in alphanumerical order by their numeric (hex-)value -/// -/// Copyright 2026 California Institute of Technology -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// -/// -/// --- -/// Portions of this file are derived from : -/// Copyright © 2024-2026 Deutsches Zentrum für Luft- und Raumfahrt e.V. -/// (DLR). -/// Copyright © 2024-2025 OxidOS Automotive SRL. +// All opcodes, in alphanumerical order by their numeric (hex-)value +// +// Copyright 2026 California Institute of Technology +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// --- +// Portions of this file are derived from https://github.com/DLR-FT/wasm-interpreter: +// Copyright © 2024-2026 Deutsches Zentrum für Luft- und Raumfahrt e.V. +// (DLR). +// Copyright © 2024-2025 OxidOS Automotive SRL. pub(crate) const UNREACHABLE: u8 = 0x00; pub(crate) const NOP: u8 = 0x01; pub(crate) const BLOCK: u8 = 0x02; diff --git a/src/types.rs b/src/types.rs index f5e581a..ee9af0e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -226,7 +226,7 @@ impl Limit { 0x01 => { let min = wasm.read_u32()?; - // Note: We are disabling `max` memory size since we don't support memory.grow + // A maximum is present: read it and ensure it is not smaller than the minimum. let max = wasm.read_u32()?; if max < min { return Err(ValidationError::InvalidMaxLimit); diff --git a/src/util/vec.rs b/src/util/vec.rs index 583fb6c..7e75ba0 100644 --- a/src/util/vec.rs +++ b/src/util/vec.rs @@ -32,18 +32,7 @@ macro_rules! vec { () => ( $crate::Vec::zero() ); - ($elem:expr; $n:expr) => ( - $crate::Vec::from_elem($elem, $n) - ); ($($x:expr),+ $(,)?) => ( - // Using `write_box_via_move` produces a dramatic improvement in stack usage for unoptimized - // programs using this code path to construct large Vecs. We can't use `write_via_move` - // because this entire invocation has to remain a call chain without `let` bindings, or else - // inference and temporary lifetimes change and things break (see `vec-macro-rvalue-scope`, - // `vec-macro-coercions`, and `autoderef-vec-box-fn-36786` tests). - // - // `box_assume_init_into_vec_unsafe` isn't actually safe but the way we use it here is. We - // can't use an unsafe block as that would also wrap `$x`. $crate::Vec::from_array([$($x),+]).unwrap() ); } @@ -206,9 +195,6 @@ impl Vec { /// Removes the last element from a vector and returns it, or [`None`] if it /// is empty. /// - /// If you'd like to pop the first element, consider using - /// `VecDeque::pop_front` instead. - /// /// # Examples /// /// ``` @@ -239,6 +225,11 @@ impl Vec { self } + /// Converts the vector into a boxed slice. + /// + /// # Panics + /// + /// Panics if the vector's length does not equal its capacity. pub fn into_boxed_slice(self) -> Box<[T], A> { assert_eq!(self.capacity(), self.len()); diff --git a/tests/core/br_table.wast b/tests/core/br_table.wast index c3d1405..34098a5 100644 --- a/tests/core/br_table.wast +++ b/tests/core/br_table.wast @@ -1589,4 +1589,4 @@ (block (br_table 0 0 0x10000001 (i32.const 1))) )) "unknown label" -) \ No newline at end of file +) diff --git a/tests/regression/host_func_invalid_should_panic_none.wast b/tests/regression/host_func_invalid_should_panic_none.wast index f46a3cc..2a65335 100644 --- a/tests/regression/host_func_invalid_should_panic_none.wast +++ b/tests/regression/host_func_invalid_should_panic_none.wast @@ -6,5 +6,8 @@ (call $invalid_should_return_none) unreachable) ) -;; This function should cause a panic because the host function is invalid +;; Invoking "call" panics because the host function is invalid, so the +;; assert_return below is never reached. The panic is the actual pass +;; criterion, asserted by the #[should_panic] attribute on +;; host_func_invalid_should_panic_none in tests/regression_integration.rs. (assert_return (invoke "call") (i32.const 0)) diff --git a/tests/regression/host_func_invalid_should_panic_some.wast b/tests/regression/host_func_invalid_should_panic_some.wast index bb2b5e6..8ac4c6e 100644 --- a/tests/regression/host_func_invalid_should_panic_some.wast +++ b/tests/regression/host_func_invalid_should_panic_some.wast @@ -6,5 +6,8 @@ (call $invalid_should_return_some) unreachable) ) -;; This function should cause a panic because the host function is invalid +;; Invoking "call" panics because the host function is invalid, so the +;; assert_return below is never reached. The panic is the actual pass +;; criterion, asserted by the #[should_panic] attribute on +;; host_func_invalid_should_panic_some in tests/regression_integration.rs. (assert_return (invoke "call") (i32.const 0)) diff --git a/tests/util/spectest.rs b/tests/util/spectest.rs index d8ee599..42e0643 100644 --- a/tests/util/spectest.rs +++ b/tests/util/spectest.rs @@ -321,6 +321,8 @@ impl WasmStream for ByteStream { const MAX_CODE_PAGES: u32 = 256; const MAX_CONTROL_FRAMES: usize = 128; const MAX_STACK_DEPTH: usize = 256; +/// Instruction budget for a single invoke/resume, used to catch infinite loops. +const MAX_INVOKE_FUEL: usize = 10_000_000; /// Builds the set of host modules an engine is instantiated with. A factory /// (rather than a `Vec`) is required because the engine is rebuilt on every @@ -644,9 +646,6 @@ fn clone_module(module: &Module) -> Module { } } -// We need to add a method to Store to support pushing modules -// For now, TestContext will manage store cloning by saving/restoring the entire Store - fn load_module( ctx: &mut TestContext, module_name: Option, @@ -751,7 +750,7 @@ fn invoke_function_resume( let result = { let _locked = enter_locked(); ctx.engine.resume(resume_value); - test_runner.run(ctx.code_builder.pages(), &mut ctx.engine, 10000000) + test_runner.run(ctx.code_builder.pages(), &mut ctx.engine, MAX_INVOKE_FUEL) }; // Get the return types we saved when the function paused @@ -854,7 +853,7 @@ fn invoke_function_normal( let result = { let _locked = enter_locked(); ctx.engine.invoke(f_ref, ¶ms).unwrap(); - test_runner.run(ctx.code_builder.pages(), &mut ctx.engine, 10000000) + test_runner.run(ctx.code_builder.pages(), &mut ctx.engine, MAX_INVOKE_FUEL) }; // Check the result @@ -926,29 +925,6 @@ fn get_global(ctx: &TestContext, module_name: &Option, field: &str) -> V } fn check_trap_reason(reason: TrapReason, text: &str) { - /* - RuntimeError::Trap(TrapError::DivideBy0) => Ok("integer divide by zero"), - RuntimeError::Trap(TrapError::UnrepresentableResult) => Ok("integer overflow"), - RuntimeError::Trap(TrapError::BadConversionToInteger) => { - Ok("invalid conversion to integer") - } - RuntimeError::Trap(TrapError::ReachedUnreachable) => Ok("unreachable"), - RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds) => { - Ok("out of bounds memory access") - } - RuntimeError::Trap(TrapError::TableOrElementAccessOutOfBounds) => { - Ok("out of bounds table access") - } - RuntimeError::Trap(TrapError::UninitializedElement) => Ok("uninitialized element"), - RuntimeError::Trap(TrapError::SignatureMismatch) => Ok("indirect call type mismatch"), - RuntimeError::Trap(TrapError::TableAccessOutOfBounds) => Ok("undefined element"), - - RuntimeError::StackExhaustion => Ok("call stack exhausted"), - RuntimeError::ModuleNotFound => Ok("module not found"), - RuntimeError::FunctionNotFound => Err(WastError::UnrepresentedRuntimeError), - RuntimeError::HostFunctionSignatureMismatch => Ok("host function signature mismatch"), - - */ match (reason, text) { (TrapReason::Unreachable, "unreachable") => {} (TrapReason::DivideByZero, "integer divide by zero") => {} From bea6b08d9d3e20d23865dfec67a01e7e3ae8271f Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Thu, 3 Sep 2026 07:26:03 -0700 Subject: [PATCH 2/7] More changes including doc fixes, result returns, general cleanup --- .github/actions/install-wabt/action.yml | 49 + .github/workflows/ci.yml | 71 +- .github/workflows/release.yml | 20 +- Cargo.toml | 4 +- README.md | 2 + crates/fuzzing/src/oracles.rs | 41 +- crates/spacewasi/src/main.rs | 73 +- crates/spacewasi/src/wasi_preview1.rs | 1617 +++-------------- crates/spacewasi/tests/integration.rs | 74 + crates/spacewasm_c_api/build.rs | 24 + crates/spacewasm_c_api/include/config.rs | 3 + crates/spacewasm_c_api/src/capi.rs | 71 +- crates/spacewasm_c_api/src/global_alloc.rs | 16 +- crates/spacewasm_c_api/src/host.rs | 9 +- crates/spacewasm_c_api/src/lib.rs | 14 +- crates/spacewasm_c_api/src/status.rs | 9 +- crates/spacewasm_c_api/src/stream.rs | 6 +- crates/spacewasm_c_api/src/tests.rs | 236 ++- crates/spacewasm_c_api/src/value.rs | 61 +- .../examples/ctest_suite.c | 27 +- crates/spacewasm_std/benches/coremark.rs | 3 +- crates/spacewasm_std/src/main.rs | 104 +- .../spacewasm_util/src/bin/spacewasm-trace.rs | 23 +- crates/spacewasm_util/src/file.rs | 86 +- crates/spacewasm_util/src/lib.rs | 6 +- docs/ir.md | 2 + fuzz/Cargo.toml | 2 +- src/code.rs | 50 +- src/compiler.rs | 2 +- src/constant.rs | 33 +- src/error.rs | 3 +- src/host.rs | 89 +- src/imports.rs | 10 +- src/interpreter.rs | 147 +- src/interpreter_tests.rs | 70 +- src/ir_reader.rs | 32 +- src/lib.rs | 48 +- src/memory.rs | 59 +- src/module.rs | 74 +- src/reader.rs | 189 +- src/store.rs | 24 +- src/stream.rs | 6 +- src/text.rs | 40 +- src/types.rs | 41 +- src/util/alloc.rs | 187 +- src/util/box_.rs | 19 +- src/util/inner_vec.rs | 112 +- src/util/paging.rs | 75 +- src/util/rc.rs | 51 +- src/util/vec.rs | 220 ++- tests/statistics_integration.rs | 267 --- tests/util/spectest.rs | 245 ++- tests/validation_rejections_integration.rs | 58 +- 53 files changed, 2148 insertions(+), 2656 deletions(-) create mode 100644 .github/actions/install-wabt/action.yml delete mode 100644 tests/statistics_integration.rs diff --git a/.github/actions/install-wabt/action.yml b/.github/actions/install-wabt/action.yml new file mode 100644 index 0000000..23ef5f4 --- /dev/null +++ b/.github/actions/install-wabt/action.yml @@ -0,0 +1,49 @@ +name: Install WABT +description: > + Download and install the WebAssembly Binary Toolkit (WABT) + +runs: + using: composite + steps: + - name: Install WABT (WebAssembly Binary Toolkit) + shell: bash + run: | + set -euo pipefail + # WABT release to install. The SHA-256 pins below are specific to this + # version -- bump both together (checksums come from the upstream + # release page) or verification will (correctly) fail. + WABT_VERSION=1.0.41 + # Select the upstream release asset for this runner. macOS x64 is handled + # defensively but intentionally has no pinned checksum: no Intel-mac + # runner is in the matrix, so add its checksum before scheduling one. + if [ "$RUNNER_OS" == "macOS" ]; then + if [ "$RUNNER_ARCH" == "ARM64" ]; then + WABT_PLATFORM="macos-arm64" + else + WABT_PLATFORM="macos-x64" + fi + else + # Linux + if [ "$RUNNER_ARCH" == "ARM64" ]; then + WABT_PLATFORM="linux-arm64" + else + WABT_PLATFORM="linux-x64" + fi + fi + case "$WABT_PLATFORM" in + linux-x64) WABT_SHA256=83f8122e924745fcd70636e3594bc01c4c47f2d4c8f3c63b5d70d3f83a482677 ;; + linux-arm64) WABT_SHA256=5e35416ee8725dc7cc0572e4392a8117cbf008b0e34c0db65c75506b0299cdbf ;; + macos-arm64) WABT_SHA256=e5269d6bbe05dfeb179e4f21111b3a641d6ccaa38b0b21d472ae5c65f8c4ff5d ;; + *) echo "::error::No pinned WABT checksum for platform $WABT_PLATFORM"; exit 1 ;; + esac + tarball="wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz" + wget -q "https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/${tarball}" + # macOS runners ship `shasum` (Perl) rather than GNU coreutils' + # `sha256sum`; both accept the same " " verify format. + if command -v sha256sum >/dev/null 2>&1; then + echo "${WABT_SHA256} ${tarball}" | sha256sum -c - + else + echo "${WABT_SHA256} ${tarball}" | shasum -a 256 -c - + fi + tar -xzf "${tarball}" + sudo cp "wabt-${WABT_VERSION}/bin/"* /usr/local/bin/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1130ac..5a3bd19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,41 +55,7 @@ jobs: components: rustfmt, clippy - name: Install WABT (WebAssembly Binary Toolkit) - run: | - set -euo pipefail - WABT_VERSION=1.0.41 - if [ "$RUNNER_OS" == "macOS" ]; then - if [ "$RUNNER_ARCH" == "ARM64" ]; then - WABT_PLATFORM="macos-arm64" - else - WABT_PLATFORM="macos-x64" - fi - else - # Linux - if [ "$RUNNER_ARCH" == "ARM64" ]; then - WABT_PLATFORM="linux-arm64" - else - WABT_PLATFORM="linux-x64" - fi - fi - case "$WABT_PLATFORM" in - linux-x64) WABT_SHA256=83f8122e924745fcd70636e3594bc01c4c47f2d4c8f3c63b5d70d3f83a482677 ;; - linux-arm64) WABT_SHA256=5e35416ee8725dc7cc0572e4392a8117cbf008b0e34c0db65c75506b0299cdbf ;; - macos-arm64) WABT_SHA256=e5269d6bbe05dfeb179e4f21111b3a641d6ccaa38b0b21d472ae5c65f8c4ff5d ;; - *) echo "::error::No pinned WABT checksum for platform $WABT_PLATFORM"; exit 1 ;; - esac - tarball="wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz" - wget -q "https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/${tarball}" - # macOS runners ship `shasum` (Perl) rather than GNU coreutils' - # `sha256sum`; both accept the same " " verify format. - if command -v sha256sum >/dev/null 2>&1; then - echo "${WABT_SHA256} ${tarball}" | sha256sum -c - - else - echo "${WABT_SHA256} ${tarball}" | shasum -a 256 -c - - fi - tar -xzf "${tarball}" - sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ - shell: bash + uses: ./.github/actions/install-wabt - name: Cache Rust dependencies # Pinned commit resolved from the annotated Swatinem/rust-cache@v2 tag. @@ -140,16 +106,7 @@ jobs: targets: i686-unknown-linux-gnu - name: Install WABT (WebAssembly Binary Toolkit) - run: | - set -euo pipefail - WABT_VERSION=1.0.41 - WABT_SHA256=83f8122e924745fcd70636e3594bc01c4c47f2d4c8f3c63b5d70d3f83a482677 - tarball="wabt-${WABT_VERSION}-linux-x64.tar.gz" - wget -q "https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/${tarball}" - echo "${WABT_SHA256} ${tarball}" | sha256sum -c - - tar -xzf "${tarball}" - sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ - shell: bash + uses: ./.github/actions/install-wabt # Only the interpreter library is deployed to the 32-bit target, so the # host-only workspace tooling is not built here. Running the suite on a @@ -236,16 +193,7 @@ jobs: uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c - name: Install WABT (WebAssembly Binary Toolkit) - run: | - set -euo pipefail - WABT_VERSION=1.0.41 - WABT_PLATFORM="linux-x64" - WABT_SHA256=83f8122e924745fcd70636e3594bc01c4c47f2d4c8f3c63b5d70d3f83a482677 - tarball="wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz" - wget -q "https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/${tarball}" - echo "${WABT_SHA256} ${tarball}" | sha256sum -c - - tar -xzf "${tarball}" - sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ + uses: ./.github/actions/install-wabt - name: Cache Rust dependencies # Pinned commit resolved from the annotated Swatinem/rust-cache@v2 tag. @@ -339,7 +287,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c with: - toolchain: nightly + toolchain: nightly-2026-08-30 components: miri - name: Run Miri @@ -362,16 +310,7 @@ jobs: components: llvm-tools-preview - name: Install WABT (WebAssembly Binary Toolkit) - run: | - set -euo pipefail - WABT_VERSION=1.0.41 - WABT_PLATFORM="linux-x64" - WABT_SHA256=83f8122e924745fcd70636e3594bc01c4c47f2d4c8f3c63b5d70d3f83a482677 - tarball="wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz" - wget -q "https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/${tarball}" - echo "${WABT_SHA256} ${tarball}" | sha256sum -c - - tar -xzf "${tarball}" - sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ + uses: ./.github/actions/install-wabt - name: Install cargo-llvm-cov uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c055df1..0045f25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,16 +44,7 @@ jobs: fi - name: Install WABT (WebAssembly Binary Toolkit) - run: | - set -euo pipefail - WABT_VERSION=1.0.41 - WABT_PLATFORM="linux-x64" - WABT_SHA256=83f8122e924745fcd70636e3594bc01c4c47f2d4c8f3c63b5d70d3f83a482677 - tarball="wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz" - wget -q "https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/${tarball}" - echo "${WABT_SHA256} ${tarball}" | sha256sum -c - - tar -xzf "${tarball}" - sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ + uses: ./.github/actions/install-wabt - name: Run tests run: cargo test --workspace --verbose @@ -64,11 +55,18 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - name: Verify spacewasm_c_api packaging + run: | + cargo build -p spacewasm_c_api --release --verbose + cargo package -p spacewasm_c_api --allow-dirty --no-verify + - name: Publish spacewasm_c_api to crates.io # Published after spacewasm since it depends on the exact matching # release. cargo waits for the dependency to be available in the index. # --allow-dirty: the injected version is an uncommitted working-tree change. - # --no-verify: Work around 'unwinding panics are not supported without std' + # --no-verify: skips cargo's verify build, which fails with "unwinding + # panics are not supported without std" for this no_std `panic = "abort"` + # staticlib/cdylib. run: cargo publish -p spacewasm_c_api --allow-dirty --no-verify env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index d25d7f7..a5125cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,8 @@ strict-assertions = [] libm = "=0.2.16" [dev-dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde = { version = "=1.0.228", features = ["derive"] } +serde_json = "=1.0.150" spacewasm = { path = ".", features = ["strict-assertions"] } # The C library is a `no_std` staticlib/cdylib and cannot unwind (no `std` in the diff --git a/README.md b/README.md index 67f1652..e8be0eb 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ Here are a couple of limitations that may be relevant to developers of Wasm modu | IR Code | 8 GiB | Compiled IR, not raw bytecode. This limit is across all modules in the store. The IR / Bytecode ratio is printed in `spacewasm_std` as the "compilation ratio". It is difficult to estimate this upfront because it varies on the types of instructions used. | | Function parameters | 255 32-bit words | Per function. | | Local variables | 65,535 32-bit words | Per function. | +| Import name length | 32 bytes | Applies to each import's module name and field name; a longer name is rejected at decode time. | +| Custom section name | 32 bytes | The name of each custom section (e.g. the `name` section); a longer name is rejected at decode time. | ## Benchmarking diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 2cb031e..c4d5dbc 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -70,10 +70,8 @@ impl WasmStream for ByteStream { if let Some(ref mut vec) = self.buffer { self.consumed = true; - let inner = InnerVec { - ptr: vec.as_mut_ptr(), - capacity: vec.len() as u32, - len: vec.len() as u32, + let inner = unsafe { + InnerVec::from_raw_parts(vec.as_mut_ptr(), vec.len() as u32, vec.len() as u32) }; Ok(Some(inner)) } else { @@ -126,13 +124,6 @@ unsafe impl Allocator for SystemAllocator { unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { std::alloc::dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: 0, - pad_bytes: 0, - } - } } /// A model of the production `PageAllocator` for fuzzing, backing guest memory. @@ -201,7 +192,7 @@ fn validate_module(wasm: &[u8]) -> Result<(), ValidationError> { let mut code_builder = CodeBuilder::new(CompilerOptions { allow_memory_grow: true, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_CODE_PAGES, }) .unwrap(); @@ -270,25 +261,8 @@ fn load_module(wasm: &[u8]) -> bool { /// differ (each need only match its label under subtyping). A `br_table` in /// unreachable code whose targets share arity but differ in type is therefore /// valid under 2.0 (wasmi accepts) and invalid under 1.0 (SpaceWasm rejects with -/// `BlockResultTypeMismatch`). +/// `BrTableResultTypeMismatch`). /// See . -/// -/// Variant granularity is sound for the first three groups because this classifier -/// is only consulted when wasmi *accepted* the module: the validation-error -/// subcases that share those variants (e.g. a non-`i32` element/data offset -/// expression, an unknown section id other than 12, or an export of a nonexistent -/// memory index) make wasmi reject too, so they never reach this check. -/// -/// `BlockResultTypeMismatch` is coarser. SpaceWasm also emits it for block-end -/// stack-height mismatches and a result-typed `if` without an `else` -- errors -/// invalid under *both* spec versions, so wasmi rejects them too and they do not -/// reach this check today. But that safety rests on SpaceWasm and wasmi agreeing, -/// not on a spec relaxation, so allow-listing the whole variant gives up the -/// fuzzer's ability to flag a *hypothetical* SpaceWasm over-strictness bug that -/// rejected an otherwise-1.0-valid module with this same variant (e.g. a dead-code -/// stack-height check). Narrowing it to only the `br_table` subcase would require -/// a dedicated error variant in the production validator; this classifier can only -/// discriminate on the variant it is handed. #[cfg(feature = "differential")] fn is_benign_rejection(err: &ValidationError) -> bool { matches!( @@ -312,9 +286,8 @@ fn is_benign_rejection(err: &ValidationError) -> bool { | ValidationError::InvalidMemIndex // Spec-version difference: a `br_table` whose targets share arity but // differ in type is valid under wasm 2.0 (wasmi) and invalid under wasm - // 1.0 (SpaceWasm). Coarse -- this variant also covers block-end checks - // invalid under both versions; see the doc comment above. - | ValidationError::BlockResultTypeMismatch + // 1.0 (SpaceWasm). + | ValidationError::BrTableResultTypeMismatch ) } @@ -377,7 +350,7 @@ pub fn no_traps(wasm: &[u8]) { // Compile module with reduced code pages let mut code_builder = CodeBuilder::new(CompilerOptions { allow_memory_grow: true, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_CODE_PAGES, }) .unwrap(); diff --git a/crates/spacewasi/src/main.rs b/crates/spacewasi/src/main.rs index 4b0f6cc..7facb44 100644 --- a/crates/spacewasi/src/main.rs +++ b/crates/spacewasi/src/main.rs @@ -25,8 +25,18 @@ use crate::wasi_preview1::make_wasi_preview1_module; use clap::error::ErrorKind; use clap::{CommandFactory, Parser}; use spacewasm_util::{FileStream, RustSystemAllocator}; +use std::process::ExitCode; use wasi_common::sync::{Dir, WasiCtxBuilder, ambient_authority}; +/// Restores the terminal from raw mode when dropped. +struct RawTtyGuard; + +impl Drop for RawTtyGuard { + fn drop(&mut self) { + let _ = crossterm::terminal::disable_raw_mode(); + } +} + spacewasm::global_allocator!( PageAllocator, PageAllocator::new(RustSystemAllocator, 0x8_000_000) @@ -73,7 +83,7 @@ struct Args { args: Vec, } -fn main() { +fn main() -> ExitCode { let args = Args::parse(); let mut cmd = Args::command(); @@ -114,13 +124,6 @@ fn main() { } } - if args.raw_tty.unwrap_or(false) { - let Ok(_) = crossterm::terminal::enable_raw_mode() else { - eprintln!("error enabling raw terminal mode"); - std::process::exit(1); - }; - } - wasi_ctx_builder.inherit_stdio(); for dir in args.dir { @@ -162,11 +165,11 @@ fn main() { } } - let preview1_module = make_wasi_preview1_module(wasi_ctx_builder.build()); + let (preview1_module, exit_code) = make_wasi_preview1_module(wasi_ctx_builder.build()); let mut code_builder = CodeBuilder::new(CompilerOptions { allow_memory_grow: true, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_PAGES as u32, }) .unwrap(); @@ -194,9 +197,21 @@ fn main() { } }; + let module_ref = engine.push_module(module).unwrap(); + + // Enable raw terminal mode (if requested) + let _tty_guard = if args.raw_tty.unwrap_or(false) { + if crossterm::terminal::enable_raw_mode().is_err() { + eprintln!("error enabling raw terminal mode"); + return ExitCode::from(1); + } + Some(RawTtyGuard) + } else { + None + }; + // Append the module and run its start function (if any). The interpreter // reads code directly from the builder's pages. - let module_ref = engine.push_module(module).unwrap(); let init_result = match engine.module_start(module_ref) { None => InterpreterResult::Finished, Some(start) => match engine.invoke(start, &[]) { @@ -205,19 +220,24 @@ fn main() { Err(_) => unreachable!(), }, }; + // A guest may call `proc_exit` from its start function; honor the recorded + // code before treating the resulting host trap as a failure. + if let Some(code) = exit_code.get() { + return ExitCode::from(code as u8); + } match init_result { InterpreterResult::Finished => {} InterpreterResult::OutOfFuel => { eprintln!("insufficient fuel for initialization"); - std::process::exit(1); + return ExitCode::from(1); } InterpreterResult::Trap(t) => { eprintln!("trap during initialization {t:?}"); - std::process::exit(1); + return ExitCode::from(1); } InterpreterResult::Pause => { eprintln!("pause during init"); - std::process::exit(1); + return ExitCode::from(1); } } @@ -228,20 +248,20 @@ fn main() { eprintln!( "error: the provided wasm module does not correctly export a _start function" ); - std::process::exit(1); + return ExitCode::from(1); }; let ExportDesc::Func(fi) = f.desc else { eprintln!( "error: the provided wasm module does not correctly export a _start function" ); - std::process::exit(1); + return ExitCode::from(1); }; fi }; let Ref::Module(fi) = module.get_func_ref(fi).unwrap() else { eprintln!("error: the provided wasm module does not correctly export a _start function"); - std::process::exit(1); + return ExitCode::from(1); }; let mut result = match engine.invoke( @@ -259,15 +279,16 @@ fn main() { result = Interpreter.run(code_builder.pages(), &mut engine, usize::MAX) } - if args.raw_tty.unwrap_or(false) { - let Ok(_) = crossterm::terminal::disable_raw_mode() else { - eprintln!("error disabling raw terminal mode"); - std::process::exit(1); - }; + // If the guest called `proc_exit`, exit with the status it requested. + if let Some(code) = exit_code.get() { + return ExitCode::from(code as u8); } - let InterpreterResult::Finished = result else { - eprintln!("interpreter failed: {:?}", result); - std::process::exit(1); - }; + match result { + InterpreterResult::Finished => ExitCode::SUCCESS, + other => { + eprintln!("interpreter failed: {other:?}"); + ExitCode::from(1) + } + } } diff --git a/crates/spacewasi/src/wasi_preview1.rs b/crates/spacewasi/src/wasi_preview1.rs index 48f81d0..154d0e9 100644 --- a/crates/spacewasi/src/wasi_preview1.rs +++ b/crates/spacewasi/src/wasi_preview1.rs @@ -1,1396 +1,255 @@ -/// WASI bindings for spacewasi using the wasi-common interfaces -/// -/// Copyright 2026 California Institute of Technology -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// -/// -/// --- -/// Portions of this file are derived from -/// and the wasi-common crate developed by the wasmtime community. +// WASI bindings for spacewasi using the wasi-common interfaces +// +// Copyright 2026 California Institute of Technology +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// +// +// --- +// Portions of this file are derived from +// and the wasi-common crate developed by the wasmtime community. use futures::executor::block_on; -use spacewasm::{HostFunction, HostModule, Value, vec}; -use std::cell::RefCell; +use spacewasm::{HostFunction, HostFunctionBreak, HostFunctionResult, HostModule, Value, vec}; +use std::cell::{Cell, RefCell}; use std::ops::ControlFlow; use std::rc::Rc; +use wasi_common::I32Exit; use wasi_common::snapshots::preview_1::wasi_snapshot_preview1; use wiggle::GuestMemory; -pub fn make_wasi_preview1_module(wasi_ctx: wasi_common::WasiCtx) -> HostModule { - let wasi_ctx_two = Rc::new(RefCell::new(wasi_ctx)); - - HostModule { - name: "wasi_snapshot_preview1".into(), - globals: vec![], - functions: vec![ - HostFunction::new("args_get", "ii".into(), "i".into(), { - let wasi_ctx_args_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::args_get( - &mut *wasi_ctx_args_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("args_sizes_get", "ii".into(), "i".into(), { - let wasi_ctx_args_sizes_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::args_sizes_get( - &mut *wasi_ctx_args_sizes_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("environ_get", "ii".into(), "i".into(), { - let wasi_ctx_environ_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::environ_get( - &mut *wasi_ctx_environ_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("environ_sizes_get", "ii".into(), "i".into(), { - let wasi_ctx_environ_sizes_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::environ_sizes_get( - &mut *wasi_ctx_environ_sizes_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("clock_res_get", "ii".into(), "i".into(), { - let wasi_ctx_clock_res_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::clock_res_get( - &mut *wasi_ctx_clock_res_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("clock_time_get", "iIi".into(), "i".into(), { - let wasi_ctx_clock_time_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::clock_time_get( - &mut *wasi_ctx_clock_time_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_advise", "iIIi".into(), "i".into(), { - let wasi_ctx_fd_advise = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - let Some(Value::I64(a2)) = args.get(2) else { - panic!("expected i64"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_advise( - &mut *wasi_ctx_fd_advise.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_allocate", "iII".into(), "i".into(), { - let wasi_ctx_fd_allocate = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - let Some(Value::I64(a2)) = args.get(2) else { - panic!("expected i64"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_allocate( - &mut *wasi_ctx_fd_allocate.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_close", "i".into(), "i".into(), { - let wasi_ctx_fd_close = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_close( - &mut *wasi_ctx_fd_close.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_datasync", "i".into(), "i".into(), { - let wasi_ctx_fd_datasync = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_datasync( - &mut *wasi_ctx_fd_datasync.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_fdstat_get", "ii".into(), "i".into(), { - let wasi_ctx_fd_fdstat_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_fdstat_get( - &mut *wasi_ctx_fd_fdstat_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_fdstat_set_flags", "ii".into(), "i".into(), { - let wasi_ctx_fd_fdstat_set_flags = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_fdstat_set_flags( - &mut *wasi_ctx_fd_fdstat_set_flags.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_fdstat_set_rights", "iII".into(), "i".into(), { - let wasi_ctx_fd_fdstat_set_rights = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - let Some(Value::I64(a2)) = args.get(2) else { - panic!("expected i64"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_fdstat_set_rights( - &mut *wasi_ctx_fd_fdstat_set_rights.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_filestat_get", "ii".into(), "i".into(), { - let wasi_ctx_fd_filestat_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_filestat_get( - &mut *wasi_ctx_fd_filestat_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_filestat_set_size", "iI".into(), "i".into(), { - let wasi_ctx_fd_filestat_set_size = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_filestat_set_size( - &mut *wasi_ctx_fd_filestat_set_size.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_filestat_set_times", "iIIi".into(), "i".into(), { - let wasi_ctx_fd_filestat_set_times = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - let Some(Value::I64(a2)) = args.get(2) else { - panic!("expected i64"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_filestat_set_times( - &mut *wasi_ctx_fd_filestat_set_times.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_pread", "iiiIi".into(), "i".into(), { - let wasi_ctx_fd_pread = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I64(a3)) = args.get(3) else { - panic!("expected i64"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_pread( - &mut *wasi_ctx_fd_pread.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_prestat_get", "ii".into(), "i".into(), { - let wasi_ctx_fd_prestat_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_prestat_get( - &mut *wasi_ctx_fd_prestat_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_prestat_dir_name", "iii".into(), "i".into(), { - let wasi_ctx_fd_prestat_dir_name = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_prestat_dir_name( - &mut *wasi_ctx_fd_prestat_dir_name.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_pwrite", "iiiIi".into(), "i".into(), { - let wasi_ctx_fd_pwrite = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I64(a3)) = args.get(3) else { - panic!("expected i64"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_pwrite( - &mut *wasi_ctx_fd_pwrite.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_read", "iiii".into(), "i".into(), { - let wasi_ctx_fd_read = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_read( - &mut *wasi_ctx_fd_read.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_readdir", "iiiIi".into(), "i".into(), { - let wasi_ctx_fd_readdir = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I64(a3)) = args.get(3) else { - panic!("expected i64"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_readdir( - &mut *wasi_ctx_fd_readdir.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_seek", "iIii".into(), "i".into(), { - let wasi_ctx_fd_seek = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I64(a1)) = args.get(1) else { - panic!("expected i64"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_seek( - &mut *wasi_ctx_fd_seek.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_sync", "i".into(), "i".into(), { - let wasi_ctx_fd_sync = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_sync( - &mut *wasi_ctx_fd_sync.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_tell", "ii".into(), "i".into(), { - let wasi_ctx_fd_tell = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_tell( - &mut *wasi_ctx_fd_tell.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("fd_write", "iiii".into(), "i".into(), { - let wasi_ctx_fd_write = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::fd_write( - &mut *wasi_ctx_fd_write.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_create_directory", "iii".into(), "i".into(), { - let wasi_ctx_path_create_directory = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_create_directory( - &mut *wasi_ctx_path_create_directory.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_filestat_get", "iiiii".into(), "i".into(), { - let wasi_ctx_path_filestat_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_filestat_get( - &mut *wasi_ctx_path_filestat_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_filestat_set_times", "iiiiIIi".into(), "i".into(), { - let wasi_ctx_path_filestat_set_times = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I64(a4)) = args.get(4) else { - panic!("expected i64"); - }; - let Some(Value::I64(a5)) = args.get(5) else { - panic!("expected i64"); - }; - let Some(Value::I32(a6)) = args.get(6) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_filestat_set_times( - &mut *wasi_ctx_path_filestat_set_times.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - *a5, - *a6, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_link", "iiiiiii".into(), "i".into(), { - let wasi_ctx_path_link = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - let Some(Value::I32(a5)) = args.get(5) else { - panic!("expected i32"); - }; - let Some(Value::I32(a6)) = args.get(6) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_link( - &mut *wasi_ctx_path_link.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - *a5, - *a6, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_open", "iiiiiIIii".into(), "i".into(), { - let wasi_ctx_path_open = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - let Some(Value::I64(a5)) = args.get(5) else { - panic!("expected i64"); - }; - let Some(Value::I64(a6)) = args.get(6) else { - panic!("expected i64"); - }; - let Some(Value::I32(a7)) = args.get(7) else { - panic!("expected i32"); - }; - let Some(Value::I32(a8)) = args.get(8) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_open( - &mut *wasi_ctx_path_open.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - *a5, - *a6, - *a7, - *a8, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_readlink", "iiiiii".into(), "i".into(), { - let wasi_ctx_path_readlink = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - let Some(Value::I32(a5)) = args.get(5) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_readlink( - &mut *wasi_ctx_path_readlink.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - *a5, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_remove_directory", "iii".into(), "i".into(), { - let wasi_ctx_path_remove_directory = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_remove_directory( - &mut *wasi_ctx_path_remove_directory.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_rename", "iiiiii".into(), "i".into(), { - let wasi_ctx_path_rename = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - let Some(Value::I32(a5)) = args.get(5) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_rename( - &mut *wasi_ctx_path_rename.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - *a5, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_symlink", "iiiii".into(), "i".into(), { - let wasi_ctx_path_symlink = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_symlink( - &mut *wasi_ctx_path_symlink.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("path_unlink_file", "iii".into(), "i".into(), { - let wasi_ctx_path_unlink_file = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::path_unlink_file( - &mut *wasi_ctx_path_unlink_file.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("poll_oneoff", "iiii".into(), "i".into(), { - let wasi_ctx_poll_oneoff = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::poll_oneoff( - &mut *wasi_ctx_poll_oneoff.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("proc_exit", "i".into(), "".into(), { - let wasi_ctx_proc_exit = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - - let _ = block_on(wasi_snapshot_preview1::proc_exit( - &mut *wasi_ctx_proc_exit.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - )); - - std::process::exit(*a0); - } - }), - HostFunction::new("proc_raise", "i".into(), "i".into(), { - let wasi_ctx_proc_raise = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::proc_raise( - &mut *wasi_ctx_proc_raise.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("random_get", "ii".into(), "i".into(), { - let wasi_ctx_random_get = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::random_get( - &mut *wasi_ctx_random_get.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("sched_yield", "".into(), "i".into(), { - let wasi_ctx_sched_yield = Rc::clone(&wasi_ctx_two); - move |state, _| { - let code = block_on(wasi_snapshot_preview1::sched_yield( - &mut *wasi_ctx_sched_yield.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - )) - .unwrap(); - - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("sock_accept", "iii".into(), "i".into(), { - let wasi_ctx_sock_accept = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - - let code = block_on(wasi_snapshot_preview1::sock_accept( - &mut *wasi_ctx_sock_accept.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - )) - .unwrap(); +/// Pop the next argument, asserting it is an `i32`. +/// +/// The interpreter validates the guest's argument count and types against each +/// binding's declared signature before the host closure runs, so a mismatch +/// here is a host-side bug (a wrong signature string in a binding), not +/// guest-reachable input; panicking is therefore appropriate. +fn next_i32(it: &mut std::slice::Iter<'_, Value>) -> i32 { + match it.next() { + Some(Value::I32(v)) => *v, + other => panic!("host binding expected an i32 argument, got {other:?}"), + } +} - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("sock_recv", "iiiiii".into(), "i".into(), { - let wasi_ctx_sock_recv = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; - let Some(Value::I32(a5)) = args.get(5) else { - panic!("expected i32"); - }; +/// Pop the next argument, asserting it is an `i64`. See [`next_i32`]. +fn next_i64(it: &mut std::slice::Iter<'_, Value>) -> i64 { + match it.next() { + Some(Value::I64(v)) => *v, + other => panic!("host binding expected an i64 argument, got {other:?}"), + } +} - let code = block_on(wasi_snapshot_preview1::sock_recv( - &mut *wasi_ctx_sock_recv.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - *a5, - )) - .unwrap(); +/// Centralized result-mapping policy shared by every WASI host binding. +/// +/// The wiggle-generated wrappers return `Result`: +/// +/// * `Ok(errno)` — the syscall ran to completion. `errno` is the WASI status +/// returned to the guest (`0` on success, or a `wasi_snapshot_preview1` errno +/// such as `EBADF`/`EFAULT`). wiggle has *already* folded the recoverable +/// guest-memory faults into an `Errno` at this point (e.g. a borrowed pointer +/// becomes `Errno::Fault`, an invalid enum becomes `Errno::Inval`), so those +/// are handed straight back to the guest as the `i32` result. +/// * `Err(trap)` — an unrecoverable host trap that the guest cannot observe as +/// an errno: an out-of-bounds or misaligned guest pointer (which the WASI +/// spec mandates trap on), an unexpected OS-level error, etc. These abort the +/// guest via [`HostFunctionBreak::Trap`] (surfacing to the embedder as +/// `TrapReason::Host`). +/// +/// This replaces the previous per-binding `.unwrap()`, which turned every host +/// trap into a panic that unwound through the entire host process. `proc_exit` +/// is the one binding that does not route through here; it interprets its +/// `I32Exit` "error" specially (see [`proc_exit_binding`]). +fn finish(result: wiggle::anyhow::Result) -> HostFunctionResult { + match result { + Ok(code) => ControlFlow::Continue(Some(Value::I32(code))), + Err(_trap) => ControlFlow::Break(HostFunctionBreak::Trap), + } +} - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("sock_send", "iiiii".into(), "i".into(), { - let wasi_ctx_sock_send = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; - let Some(Value::I32(a2)) = args.get(2) else { - panic!("expected i32"); - }; - let Some(Value::I32(a3)) = args.get(3) else { - panic!("expected i32"); - }; - let Some(Value::I32(a4)) = args.get(4) else { - panic!("expected i32"); - }; +/// Select the argument accessor for a signature character +/// (`i` = i32, `I` = i64), matching the interpreter's signature-string alphabet. +macro_rules! next_arg { + (i, $it:expr) => { + next_i32($it) + }; + (I, $it:expr) => { + next_i64($it) + }; +} - let code = block_on(wasi_snapshot_preview1::sock_send( - &mut *wasi_ctx_sock_send.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - *a2, - *a3, - *a4, - )) - .unwrap(); +/// Generate a `wasi_snapshot_preview1` [`HostFunction`] binding. +/// +/// This folds together the three pieces that were previously copy-pasted across +/// ~45 bindings: the sound `GuestMemory::Shared` view of linear memory +/// ([`spacewasm::Memory::as_shared_cells`]), typed argument extraction, and the +/// shared error mapping ([`finish`]). +/// +/// * `$ctx` — the shared `WasiCtx` handle to clone into the closure. +/// * `$name` — the wiggle function, also used verbatim as the exported name. +/// * `$params` — the interpreter parameter-signature string. +/// * `$results` — the interpreter result-signature string. +/// * `[ .. ]` — the argument types in order (`i` = i32, `I` = i64). +macro_rules! wasi_binding { + ( + $ctx:ident, + $name:ident, + $params:literal, + $results:literal, + [ $( $ty:ident ),* $(,)? ] + ) => {{ + let ctx = Rc::clone(&$ctx); + HostFunction::new( + stringify!($name), + $params.into(), + $results.into(), + move |state, args| { + #[allow(unused_mut, unused_variables)] + let mut it = args.iter(); + finish(block_on(wasi_snapshot_preview1::$name( + &mut *ctx.borrow_mut(), + &mut GuestMemory::Shared(state.memory.as_shared_cells()), + $( next_arg!($ty, &mut it) ),* + ))) + }, + ) + }}; +} - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), - HostFunction::new("sock_shutdown", "ii".into(), "i".into(), { - let wasi_ctx_sock_shutdown = Rc::clone(&wasi_ctx_two); - move |state, args| { - let Some(Value::I32(a0)) = args.first() else { - panic!("expected i32"); - }; - let Some(Value::I32(a1)) = args.get(1) else { - panic!("expected i32"); - }; +/// The `proc_exit` binding. +/// +/// It cannot use [`wasi_binding!`]/[`finish`] because wasi-common models +/// `proc_exit` as a diverging call: its wiggle wrapper always returns `Err` — +/// an `I32Exit(code)` for a valid status, or a generic trap for an out-of-range +/// one. Rather than calling `std::process::exit` (which would skip the caller's +/// Drop-based cleanup, such as restoring the terminal from raw mode), it records +/// the requested status in `exit_code` and traps out of the interpreter. `main` +/// observes the recorded code after the run unwinds and exits with it, so all +/// RAII guards get a chance to run. An out-of-range status carries no `I32Exit`, +/// so nothing is recorded and `main` treats it as an ordinary host trap. +fn proc_exit_binding( + wasi_ctx: &Rc>, + exit_code: &Rc>>, +) -> HostFunction { + let ctx = Rc::clone(wasi_ctx); + let exit_code = Rc::clone(exit_code); + HostFunction::new("proc_exit", "i".into(), "".into(), move |state, args| { + let mut it = args.iter(); + let status = next_i32(&mut it); + if let Err(e) = block_on(wasi_snapshot_preview1::proc_exit( + &mut *ctx.borrow_mut(), + &mut GuestMemory::Shared(state.memory.as_shared_cells()), + status, + )) && let Some(exit) = e.downcast_ref::() + { + exit_code.set(Some(exit.0)); + } + ControlFlow::Break(HostFunctionBreak::Trap) + }) +} - let code = block_on(wasi_snapshot_preview1::sock_shutdown( - &mut *wasi_ctx_sock_shutdown.borrow_mut(), - &mut GuestMemory::Shared(unsafe { - core::mem::transmute::<&[u8], &[std::cell::UnsafeCell]>( - state.memory.get_slice(), - ) - }), - *a0, - *a1, - )) - .unwrap(); +/// Build the `wasi_snapshot_preview1` host module. +/// +/// Returns the module together with a shared exit-code cell. When the guest +/// calls `proc_exit`, the binding records the exit status in this cell and traps +/// out of the interpreter (see [`proc_exit_binding`]); the caller (`main`) +/// inspects the cell after the run finishes to decide the process exit code. +pub fn make_wasi_preview1_module( + wasi_ctx: wasi_common::WasiCtx, +) -> (HostModule, Rc>>) { + let wasi_ctx_two = Rc::new(RefCell::new(wasi_ctx)); + let exit_code: Rc>> = Rc::new(Cell::new(None)); - ControlFlow::Continue(Some(Value::I32(code as i32))) - } - }), + let module = HostModule { + name: "wasi_snapshot_preview1".into(), + globals: vec![], + functions: vec![ + wasi_binding!(wasi_ctx_two, args_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, args_sizes_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, environ_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, environ_sizes_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, clock_res_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, clock_time_get, "iIi", "i", [i, I, i]), + wasi_binding!(wasi_ctx_two, fd_advise, "iIIi", "i", [i, I, I, i]), + wasi_binding!(wasi_ctx_two, fd_allocate, "iII", "i", [i, I, I]), + wasi_binding!(wasi_ctx_two, fd_close, "i", "i", [i]), + wasi_binding!(wasi_ctx_two, fd_datasync, "i", "i", [i]), + wasi_binding!(wasi_ctx_two, fd_fdstat_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, fd_fdstat_set_flags, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, fd_fdstat_set_rights, "iII", "i", [i, I, I]), + wasi_binding!(wasi_ctx_two, fd_filestat_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, fd_filestat_set_size, "iI", "i", [i, I]), + wasi_binding!( + wasi_ctx_two, + fd_filestat_set_times, + "iIIi", + "i", + [i, I, I, i] + ), + wasi_binding!(wasi_ctx_two, fd_pread, "iiiIi", "i", [i, i, i, I, i]), + wasi_binding!(wasi_ctx_two, fd_prestat_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, fd_prestat_dir_name, "iii", "i", [i, i, i]), + wasi_binding!(wasi_ctx_two, fd_pwrite, "iiiIi", "i", [i, i, i, I, i]), + wasi_binding!(wasi_ctx_two, fd_read, "iiii", "i", [i, i, i, i]), + wasi_binding!(wasi_ctx_two, fd_readdir, "iiiIi", "i", [i, i, i, I, i]), + wasi_binding!(wasi_ctx_two, fd_seek, "iIii", "i", [i, I, i, i]), + wasi_binding!(wasi_ctx_two, fd_sync, "i", "i", [i]), + wasi_binding!(wasi_ctx_two, fd_tell, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, fd_write, "iiii", "i", [i, i, i, i]), + wasi_binding!(wasi_ctx_two, path_create_directory, "iii", "i", [i, i, i]), + wasi_binding!( + wasi_ctx_two, + path_filestat_get, + "iiiii", + "i", + [i, i, i, i, i] + ), + wasi_binding!( + wasi_ctx_two, + path_filestat_set_times, + "iiiiIIi", + "i", + [i, i, i, i, I, I, i] + ), + wasi_binding!( + wasi_ctx_two, + path_link, + "iiiiiii", + "i", + [i, i, i, i, i, i, i] + ), + wasi_binding!( + wasi_ctx_two, + path_open, + "iiiiiIIii", + "i", + [i, i, i, i, i, I, I, i, i] + ), + wasi_binding!( + wasi_ctx_two, + path_readlink, + "iiiiii", + "i", + [i, i, i, i, i, i] + ), + wasi_binding!(wasi_ctx_two, path_remove_directory, "iii", "i", [i, i, i]), + wasi_binding!(wasi_ctx_two, path_rename, "iiiiii", "i", [i, i, i, i, i, i]), + wasi_binding!(wasi_ctx_two, path_symlink, "iiiii", "i", [i, i, i, i, i]), + wasi_binding!(wasi_ctx_two, path_unlink_file, "iii", "i", [i, i, i]), + wasi_binding!(wasi_ctx_two, poll_oneoff, "iiii", "i", [i, i, i, i]), + proc_exit_binding(&wasi_ctx_two, &exit_code), + wasi_binding!(wasi_ctx_two, proc_raise, "i", "i", [i]), + wasi_binding!(wasi_ctx_two, random_get, "ii", "i", [i, i]), + wasi_binding!(wasi_ctx_two, sched_yield, "", "i", []), + wasi_binding!(wasi_ctx_two, sock_accept, "iii", "i", [i, i, i]), + wasi_binding!(wasi_ctx_two, sock_recv, "iiiiii", "i", [i, i, i, i, i, i]), + wasi_binding!(wasi_ctx_two, sock_send, "iiiii", "i", [i, i, i, i, i]), + wasi_binding!(wasi_ctx_two, sock_shutdown, "ii", "i", [i, i]), ], memory: spacewasm::Vec::zero(), table: spacewasm::Vec::zero(), - } + }; + + (module, exit_code) } diff --git a/crates/spacewasi/tests/integration.rs b/crates/spacewasi/tests/integration.rs index 493678c..e8029ec 100644 --- a/crates/spacewasi/tests/integration.rs +++ b/crates/spacewasi/tests/integration.rs @@ -1,6 +1,31 @@ use assert_cmd::cargo::*; use predicates::prelude::*; +/// A `.wasm` fixture written to a unique temp path for the duration of a test, +/// removed on drop. Used by the negative-path tests, which need module bytes +/// that no committed fixture provides (garbage, empty, header-only) without +/// polluting `tests/wasm/`. +struct TempWasm(std::path::PathBuf); + +impl TempWasm { + fn new(tag: &str, bytes: &[u8]) -> Self { + let mut path = std::env::temp_dir(); + path.push(format!("spacewasi_it_{}_{tag}.wasm", std::process::id())); + std::fs::write(&path, bytes).expect("write temp wasm fixture"); + TempWasm(path) + } + + fn path(&self) -> &std::path::Path { + &self.0 + } +} + +impl Drop for TempWasm { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + #[test] fn fake_file() -> Result<(), Box> { let mut cmd = cargo_bin_cmd!("spacewasi"); @@ -102,3 +127,52 @@ fn return_code() -> Result<(), Box> { Ok(()) } + +// --- Negative paths ------------------------------------------------------- +// +// These observe the CLI's own error surface (exit status + diagnostic on +// stderr) rather than just smoke-testing that a good module runs. + +#[test] +fn malformed_module_is_rejected() -> Result<(), Box> { + // Bytes whose magic number is not `\0asm`: the parser must reject them. + let wasm = TempWasm::new("malformed", b"this is definitely not a wasm module"); + + let mut cmd = cargo_bin_cmd!("spacewasi"); + cmd.arg(wasm.path()); + cmd.assert() + .failure() + .stderr(predicate::str::contains("failed to parse WASM module")); + + Ok(()) +} + +#[test] +fn empty_module_is_rejected() -> Result<(), Box> { + // An empty file has no magic/version header at all. + let wasm = TempWasm::new("empty", b""); + + let mut cmd = cargo_bin_cmd!("spacewasi"); + cmd.arg(wasm.path()); + cmd.assert() + .failure() + .stderr(predicate::str::contains("failed to parse WASM module")); + + Ok(()) +} + +#[test] +fn module_without_start_export_is_rejected() -> Result<(), Box> { + // A well-formed but empty module: the 4-byte magic and version-1 header + // with no sections. It parses cleanly yet exports no `_start`, so the CLI + // must reject it with a clear message instead of crashing. + let wasm = TempWasm::new("nostart", &[0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]); + + let mut cmd = cargo_bin_cmd!("spacewasi"); + cmd.arg(wasm.path()); + cmd.assert().failure().stderr(predicate::str::contains( + "does not correctly export a _start function", + )); + + Ok(()) +} diff --git a/crates/spacewasm_c_api/build.rs b/crates/spacewasm_c_api/build.rs index 8f16918..216629a 100644 --- a/crates/spacewasm_c_api/build.rs +++ b/crates/spacewasm_c_api/build.rs @@ -46,6 +46,30 @@ fn generate_header() { // its mtime unchanged) when the output is identical, so regenerating // does not trigger a rebuild loop. .write_to_file(&header); + + // Attatch [noreturn] to spacewasm_panic + annotate_noreturn(&header); +} + +/// Prefix the generated `spacewasm_panic` declaration with `SPACEWASM_NORETURN`. +#[cfg(feature = "codegen")] +fn annotate_noreturn(header: &Path) { + use std::fs; + + const DECL: &str = "extern void spacewasm_panic("; + const ANNOTATED: &str = "SPACEWASM_NORETURN extern void spacewasm_panic("; + + let contents = fs::read_to_string(header).expect("failed to read generated header"); + // Idempotent: nothing to do if the annotation is already present. + if contents.contains(ANNOTATED) { + return; + } + let patched = contents.replacen(DECL, ANNOTATED, 1); + assert!( + patched != contents, + "expected `{DECL}` in the generated header to annotate with SPACEWASM_NORETURN" + ); + fs::write(header, patched).expect("failed to write annotated header"); } #[cfg(not(feature = "codegen"))] diff --git a/crates/spacewasm_c_api/include/config.rs b/crates/spacewasm_c_api/include/config.rs index b96ad87..a198584 100644 --- a/crates/spacewasm_c_api/include/config.rs +++ b/crates/spacewasm_c_api/include/config.rs @@ -1,3 +1,6 @@ +// Compile-time limits for the C API build (the default configuration). +// To override this file set `SPACEWASM_CONFIG` in your `build.rs`. + /// Maximum control frame depth per function. pub const MAX_CONTROL_FRAMES: usize = 64; diff --git a/crates/spacewasm_c_api/src/capi.rs b/crates/spacewasm_c_api/src/capi.rs index fddba53..0ec41f7 100644 --- a/crates/spacewasm_c_api/src/capi.rs +++ b/crates/spacewasm_c_api/src/capi.rs @@ -51,7 +51,8 @@ impl From for CompilerOptions { fn from(o: spacewasm_compiler_options_t) -> Self { CompilerOptions { allow_memory_grow: o.allow_memory_grow, - max_backpatch_iterations: o.max_backpatch_iterations, + max_backpatch_iterations: (o.max_backpatch_iterations != 0) + .then_some(o.max_backpatch_iterations), max_code_pages: o.max_code_pages, } } @@ -128,6 +129,17 @@ impl From<&mut spacewasm_host_t> for &mut Vec { } } +const _: () = { + assert!( + core::mem::size_of::() == core::mem::size_of::>(), + "spacewasm_host_t must match Vec layout (size)" + ); + assert!( + core::mem::align_of::() == core::mem::align_of::>(), + "spacewasm_host_t must match Vec layout (align)" + ); +}; + /// Create a new host module vector of `len` size /// /// # Safety @@ -694,20 +706,26 @@ pub unsafe extern "C" fn spacewasm_set_global( None => return status::SPACEWASM_ERR_NOT_FOUND, }; - if ValType::from(value.tag) != global.type_.ty { + let Some(value_val) = value.try_to_value() else { + return status::SPACEWASM_ERR_BAD_ARG; + }; + + let (ty, raw) = match value_val { + Value::I32(i) => (ValType::I32, RawValue::from_i32(i)), + Value::I64(i) => (ValType::I64, RawValue::from_i64(i)), + Value::F32(f) => (ValType::F32, RawValue::from_f32(f)), + Value::F64(f) => (ValType::F64, RawValue::from_f64(f)), + }; + + if ty != global.type_.ty { return status::SPACEWASM_ERR_GLOBAL_TYPE_MISMATCH; } if !global.type_.mutable { - return status::SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE; + return status::SPACEWASM_ERR_GLOBAL_NOT_MUTABLE; } - global.value = match value.to_value() { - Value::I32(i) => RawValue::from_i32(i), - Value::I64(i) => RawValue::from_i64(i), - Value::F32(f) => RawValue::from_f32(f), - Value::F64(f) => RawValue::from_f64(f), - }; + global.value = raw; status::SPACEWASM_OK } @@ -752,13 +770,13 @@ pub unsafe extern "C" fn spacewasm_invoke( return status::SPACEWASM_ERR_CAPACITY; } - // Marshal parameters. `from_raw_parts` requires a non-null pointer even for - // a zero-length slice, so only build the slice when there are entries (the - // contract permits a null `params` when `n == 0`). if n != 0 { let slice = unsafe { core::slice::from_raw_parts(params, n) }; for (i, v) in slice.iter().enumerate() { - buf[i] = v.to_value(); + let Some(val) = v.try_to_value() else { + return status::SPACEWASM_ERR_BAD_ARG; + }; + buf[i] = val; } } @@ -818,8 +836,10 @@ pub unsafe extern "C" fn spacewasm_resume(engine: *mut CEngine) -> spacewasm_sta return status::SPACEWASM_ERR_NULL_ARG; }; - cengine.engine.resume(None); - status::SPACEWASM_OK + match cengine.engine.resume(None) { + Ok(()) => status::SPACEWASM_OK, + Err(_) => status::SPACEWASM_ERR_WRONG_STATE, + } } /// Resume the interpreter from a paused state. @@ -837,8 +857,14 @@ pub unsafe extern "C" fn spacewasm_resume_value( return status::SPACEWASM_ERR_NULL_ARG; }; - cengine.engine.resume(Some(resume_value.into())); - status::SPACEWASM_OK + let Some(value) = resume_value.try_to_value() else { + return status::SPACEWASM_ERR_BAD_ARG; + }; + + match cengine.engine.resume(Some(value)) { + Ok(()) => status::SPACEWASM_OK, + Err(_) => status::SPACEWASM_ERR_WRONG_STATE, + } } /// Reset the engine back to an idle state, discarding any in-progress or @@ -862,6 +888,12 @@ pub unsafe extern "C" fn spacewasm_reset(engine: *mut CEngine) -> spacewasm_stat /// Fetch the result of the last completed call, coerced to `expected`, into /// `out`. /// +/// # Silent type coercion +/// +/// The core engine stores a completed call's result as an untagged +/// [`RawValue`]. The function signature must be checked before invoking and the return +/// value must be extracted (in this function) using the proper `expected` type. +/// /// # Safety /// `engine` must be live; `out` valid. #[unsafe(no_mangle)] @@ -876,10 +908,13 @@ pub unsafe extern "C" fn spacewasm_get_result( if out.is_null() { return status::SPACEWASM_ERR_NULL_ARG; } + let Ok(expected_ty) = ValType::try_from(&expected) else { + return status::SPACEWASM_ERR_BAD_ARG; + }; match cengine .engine .result - .map(|raw| spacewasm_value_t::from_raw(raw, ValType::from(expected))) + .map(|raw| spacewasm_value_t::from_raw(raw, expected_ty)) { Some(v) => { unsafe { *out = v }; diff --git a/crates/spacewasm_c_api/src/global_alloc.rs b/crates/spacewasm_c_api/src/global_alloc.rs index db31268..decc506 100644 --- a/crates/spacewasm_c_api/src/global_alloc.rs +++ b/crates/spacewasm_c_api/src/global_alloc.rs @@ -6,7 +6,8 @@ use core::ptr; use core::sync::atomic::{AtomicPtr, Ordering}; use crate::config::{GLOBAL_ALLOCATOR_MAX_PAGES, GLOBAL_ALLOCATOR_PAGE_SIZE}; -use spacewasm::{AllocError, Allocator, MemoryStatistics, PageAllocator}; +use crate::spacewasm_status_t; +use spacewasm::{AllocError, Allocator, PageAllocator}; /// Allocate `size` bytes aligned to `align`. Return NULL on failure. Per page allocation. pub type spacewasm_global_alloc_fn_t = @@ -81,13 +82,6 @@ unsafe impl Allocator for CPageBackend { ) }; } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: 0, - pad_bytes: 0, - } - } } static BACKEND: CPageBackend = CPageBackend::new(); @@ -108,9 +102,9 @@ pub extern "C" fn spacewasm_set_global_allocator( alloc: spacewasm_global_alloc_fn_t, dealloc: spacewasm_global_dealloc_fn_t, userdata: *mut c_void, -) -> i32 { +) -> spacewasm_status_t { let (Some(alloc), Some(dealloc)) = (alloc, dealloc) else { - return 1; // a null callback + return spacewasm_status_t::SPACEWASM_ERR_BAD_ARG; }; // Publish userdata before the callbacks so a reader that observes a @@ -118,5 +112,5 @@ pub extern "C" fn spacewasm_set_global_allocator( BACKEND.userdata.store(userdata, Ordering::Release); BACKEND.dealloc.store(dealloc as *mut (), Ordering::Release); BACKEND.alloc.store(alloc as *mut (), Ordering::Release); - 0 + spacewasm_status_t::SPACEWASM_OK } diff --git a/crates/spacewasm_c_api/src/host.rs b/crates/spacewasm_c_api/src/host.rs index efd0cf0..6b9048e 100644 --- a/crates/spacewasm_c_api/src/host.rs +++ b/crates/spacewasm_c_api/src/host.rs @@ -85,7 +85,7 @@ impl CHostFunction { /// Invoke the C callback for a guest→host call: marshal the arguments, /// expose the state as an opaque caller handle, and translate the result /// back into spacewasm's `ControlFlow` outcome. - pub(crate) fn call(&self, state: &Engine, args: &[Value]) -> HostFunctionResult { + pub(crate) fn call(&self, state: &mut Engine, args: &[Value]) -> HostFunctionResult { if args.len() > MAX_HOST_PARAMS { return ControlFlow::Break(HostFunctionBreak::Trap); } @@ -106,7 +106,7 @@ impl CHostFunction { // Expose the borrowed state as an opaque caller pointer. The callback // may only use it for the duration of this call. - let caller = state as *const Engine as *mut SpacewasmCaller; + let caller = state as *mut Engine as *mut SpacewasmCaller; // SAFETY: `f` is a valid C function pointer supplied at registration. let outcome = unsafe { @@ -122,7 +122,10 @@ impl CHostFunction { match outcome { spacewasm_hostcall_result_t::SPACEWASM_CONTINUE_NONE => ControlFlow::Continue(None), spacewasm_hostcall_result_t::SPACEWASM_CONTINUE_SOME => { - ControlFlow::Continue(Some(out_result.to_value())) + match out_result.try_to_value() { + Some(v) => ControlFlow::Continue(Some(v)), + None => panic!("invalid out_result type"), + } } spacewasm_hostcall_result_t::SPACEWASM_TRAP => { ControlFlow::Break(HostFunctionBreak::Trap) diff --git a/crates/spacewasm_c_api/src/lib.rs b/crates/spacewasm_c_api/src/lib.rs index 85b482b..19629bf 100644 --- a/crates/spacewasm_c_api/src/lib.rs +++ b/crates/spacewasm_c_api/src/lib.rs @@ -27,20 +27,8 @@ mod tests; pub use alloc::{CAllocator, spacewasm_alloc_fn_t, spacewasm_dealloc_fn_t, spacewasm_realloc_fn_t}; pub use capi::{CEngine, spacewasm_compiler_options_t}; #[cfg(feature = "provide-global-allocator")] -pub use global_alloc::{ - spacewasm_global_alloc_fn_t, spacewasm_global_dealloc_fn_t, spacewasm_set_global_allocator, -}; +pub use global_alloc::{spacewasm_global_dealloc_fn_t, spacewasm_set_global_allocator}; pub use host::{SpacewasmCaller, spacewasm_host_fn_t, spacewasm_hostcall_result_t}; pub use status::{spacewasm_run_status_t, spacewasm_status_t, spacewasm_trap_t}; pub use stream::{spacewasm_read_fn_t, spacewasm_read_result_t}; pub use value::{spacewasm_valtype_t, spacewasm_value_t}; - -/// FFI-safe copy of [`spacewasm::MemoryStatistics`] (already `#[repr(C)]`). -pub use spacewasm::MemoryStatistics as spacewasm_memory_statistics_t; - -/// Global allocator statistics. Independent of the interpreter configuration, -/// so it takes no const-generic parameters. -#[unsafe(no_mangle)] -pub extern "C" fn spacewasm_memory_statistics() -> spacewasm_memory_statistics_t { - spacewasm::Allocator::memory_statistics(&spacewasm::GlobalAllocator) -} diff --git a/crates/spacewasm_c_api/src/status.rs b/crates/spacewasm_c_api/src/status.rs index 097c648..2cadb3b 100644 --- a/crates/spacewasm_c_api/src/status.rs +++ b/crates/spacewasm_c_api/src/status.rs @@ -79,7 +79,8 @@ pub enum spacewasm_status_t { SPACEWASM_ERR_LABEL_JUMP_TOO_LARGE = 100, SPACEWASM_ERR_TYPE_MISMATCH = 101, SPACEWASM_ERR_BLOCK_RESULT_TYPE_MISMATCH = 102, - SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 103, + SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH = 103, + SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 104, // Parse / validation errors - Memory and table validation SPACEWASM_ERR_ILLEGAL_MEMORY_GROW = 112, @@ -114,7 +115,7 @@ pub enum spacewasm_status_t { SPACEWASM_ERR_TABLE_IMPORT_NOT_FOUND = 147, SPACEWASM_ERR_FUNCTION_IMPORT_OUT_OF_RANGE = 148, SPACEWASM_ERR_FUNCTION_IMPORT_TYPE_MISMATCH = 149, - SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE = 150, + SPACEWASM_ERR_GLOBAL_NOT_MUTABLE = 150, SPACEWASM_ERR_GLOBAL_IMPORT_TYPE_MISMATCH = 151, SPACEWASM_ERR_MEMORY_IMPORT_TYPE_MISMATCH = 152, SPACEWASM_ERR_TABLE_IMPORT_TYPE_MISMATCH = 153, @@ -237,6 +238,7 @@ pub fn invoke_status(e: InvokeError) -> spacewasm_status_t { InvokeError::ParamLenMismatch => SPACEWASM_ERR_PARAM_LEN_MISMATCH, InvokeError::ParamTypeMismatch => SPACEWASM_ERR_PARAM_TYPE_MISMATCH, InvokeError::StackOverflow => SPACEWASM_ERR_STACK_OVERFLOW, + InvokeError::Busy => SPACEWASM_ERR_WRONG_STATE, } } @@ -283,6 +285,7 @@ pub fn validation_status(e: &ValidationError) -> spacewasm_status_t { ValidationError::LabelJumpTooLarge => SPACEWASM_ERR_LABEL_JUMP_TOO_LARGE, ValidationError::TypeMismatch => SPACEWASM_ERR_TYPE_MISMATCH, ValidationError::BlockResultTypeMismatch => SPACEWASM_ERR_BLOCK_RESULT_TYPE_MISMATCH, + ValidationError::BrTableResultTypeMismatch => SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH, ValidationError::FunctionResultTypeMismatch => SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH, ValidationError::IllegalMemoryGrow => SPACEWASM_ERR_ILLEGAL_MEMORY_GROW, ValidationError::InvalidElementOffset => SPACEWASM_ERR_INVALID_ELEMENT_OFFSET, @@ -316,7 +319,7 @@ pub fn validation_status(e: &ValidationError) -> spacewasm_status_t { ValidationError::TableImportNotFound => SPACEWASM_ERR_TABLE_IMPORT_NOT_FOUND, ValidationError::FunctionImportOutOfRange => SPACEWASM_ERR_FUNCTION_IMPORT_OUT_OF_RANGE, ValidationError::FunctionImportTypeMismatch => SPACEWASM_ERR_FUNCTION_IMPORT_TYPE_MISMATCH, - ValidationError::GlobalIsNotMutable => SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE, + ValidationError::GlobalNotMutable => SPACEWASM_ERR_GLOBAL_NOT_MUTABLE, ValidationError::GlobalImportTypeMismatch => SPACEWASM_ERR_GLOBAL_IMPORT_TYPE_MISMATCH, ValidationError::MemoryImportTypeMismatch => SPACEWASM_ERR_MEMORY_IMPORT_TYPE_MISMATCH, ValidationError::TableImportTypeMismatch => SPACEWASM_ERR_TABLE_IMPORT_TYPE_MISMATCH, diff --git a/crates/spacewasm_c_api/src/stream.rs b/crates/spacewasm_c_api/src/stream.rs index edccf6e..9e9b515 100644 --- a/crates/spacewasm_c_api/src/stream.rs +++ b/crates/spacewasm_c_api/src/stream.rs @@ -85,10 +85,8 @@ impl WasmStream for CallbackStream { // Hand the interpreter a borrowed view of the callback's buffer. // `capacity: 0` ensures the `Chunk` drop assertion holds and no // deallocation of borrowed memory is attempted. - Ok(Some(InnerVec { - ptr: out_buf as *mut u8, - capacity: 0, - len: out_len as u32, + Ok(Some(unsafe { + InnerVec::from_raw_parts(out_buf as *mut u8, 0, out_len as u32) })) } } diff --git a/crates/spacewasm_c_api/src/tests.rs b/crates/spacewasm_c_api/src/tests.rs index 356b3f4..26d7be8 100644 --- a/crates/spacewasm_c_api/src/tests.rs +++ b/crates/spacewasm_c_api/src/tests.rs @@ -27,7 +27,11 @@ fn ensure_global_allocator() { Some(global_dealloc), core::ptr::null_mut(), ); - assert_eq!(rc, 0, "set_global_allocator failed"); + assert_eq!( + rc, + crate::spacewasm_status_t::SPACEWASM_OK, + "set_global_allocator failed" + ); } /// A minimum alignment matching what the C suite uses (`sizeof(void*)`). @@ -453,22 +457,6 @@ fn invoke_add( Ok(unsafe { out.u.i32_ }) } -/// Load `ADD_WASM`, invoke `add(1, 2)`, and tear everything down. Used by the -/// no-leak lifecycle test. -fn run_add_once() { - let store = new_store(1024, 1, 256); - let alloc = new_guest_allocator(); - let idx = load_module_onto(alloc, store, c"main", ADD_WASM, 0).expect("load"); - let mut func = 0u32; - let st = unsafe { spacewasm_find_export_func(store, idx, c"add".as_ptr(), &mut func) }; - assert_eq!(st, status::SPACEWASM_OK, "find"); - assert_eq!(invoke_add(store, idx, func, 1, 2).expect("invoke"), 3); - unsafe { - spacewasm_destroy(store); - spacewasm_allocator_destroy(alloc); - } -} - // ---- host callback ---------------------------------------------------------- /// Host implementation of `env.add_one`: returns `param + 1`. @@ -741,7 +729,7 @@ fn global_get_set() { // Writing a const global is rejected. assert_eq!( spacewasm_set_global(store, idx, c, i32_val(1)), - status::SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE, + status::SPACEWASM_ERR_GLOBAL_NOT_MUTABLE, "set const global" ); // The const global keeps its value. @@ -1451,16 +1439,6 @@ fn null_arg_handling() { unsafe { spacewasm_destroy(store) }; } -#[test] -fn statistics_available() { - let _guard = ALLOC_LOCK.lock().unwrap(); - ensure_global_allocator(); - - // Just confirm the statistics entry point is wired and returns. - let stats = crate::spacewasm_memory_statistics(); - let _ = (stats.total_bytes, stats.pad_bytes); -} - // ---- pure status-mapping tests ---------------------------------------------- #[test] @@ -1626,6 +1604,10 @@ fn validation_error_codes_map() { BlockResultTypeMismatch, status::SPACEWASM_ERR_BLOCK_RESULT_TYPE_MISMATCH, ), + ( + BrTableResultTypeMismatch, + status::SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH, + ), ( FunctionResultTypeMismatch, status::SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH, @@ -1712,10 +1694,7 @@ fn validation_error_codes_map() { FunctionImportTypeMismatch, status::SPACEWASM_ERR_FUNCTION_IMPORT_TYPE_MISMATCH, ), - ( - GlobalIsNotMutable, - status::SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE, - ), + (GlobalNotMutable, status::SPACEWASM_ERR_GLOBAL_NOT_MUTABLE), ( GlobalImportTypeMismatch, status::SPACEWASM_ERR_GLOBAL_IMPORT_TYPE_MISMATCH, @@ -1882,7 +1861,7 @@ fn invoke_status_maps() { #[test] fn simple_error_mappers() { - use spacewasm::{HostNameError, HostFunctionError, SectionDecodeError, ValidationError}; + use spacewasm::{HostFunctionError, HostNameError, SectionDecodeError, ValidationError}; let pe = spacewasm::ParseError::new(0, SectionDecodeError::new(ValidationError::Eof)); assert_eq!(status::parse_status(&pe), status::SPACEWASM_ERR_EOF); @@ -1972,7 +1951,7 @@ fn value_round_trips_all_types() { ]; for v in values { let c = spacewasm_value_t::from_value(v); - assert_eq!(c.to_value(), v, "round trip {v:?}"); + assert_eq!(c.try_to_value().unwrap(), v, "round trip {v:?}"); } } @@ -1981,19 +1960,27 @@ fn value_from_raw_reinterprets_by_type() { use spacewasm::{RawValue, ValType, Value}; assert_eq!( - spacewasm_value_t::from_raw(RawValue::from_i32(-1), ValType::I32).to_value(), + spacewasm_value_t::from_raw(RawValue::from_i32(-1), ValType::I32) + .try_to_value() + .unwrap(), Value::I32(-1) ); assert_eq!( - spacewasm_value_t::from_raw(RawValue::from_i64(9), ValType::I64).to_value(), + spacewasm_value_t::from_raw(RawValue::from_i64(9), ValType::I64) + .try_to_value() + .unwrap(), Value::I64(9) ); assert_eq!( - spacewasm_value_t::from_raw(RawValue::from_f32(1.5), ValType::F32).to_value(), + spacewasm_value_t::from_raw(RawValue::from_f32(1.5), ValType::F32) + .try_to_value() + .unwrap(), Value::F32(1.5) ); assert_eq!( - spacewasm_value_t::from_raw(RawValue::from_f64(6.5), ValType::F64).to_value(), + spacewasm_value_t::from_raw(RawValue::from_f64(6.5), ValType::F64) + .try_to_value() + .unwrap(), Value::F64(6.5) ); } @@ -2010,7 +1997,7 @@ fn valtype_conversions_both_directions() { ]; for (vt, c) in pairs { assert_eq!(spacewasm_valtype_t::from(vt), c); - assert_eq!(ValType::from(c), vt); + assert_eq!(ValType::try_from(&c).unwrap(), vt); } } @@ -2269,6 +2256,127 @@ fn reset_abandons_in_progress_call() { } } +/// Resuming an engine that is not paused (nothing is awaiting a host result) is +/// a state error, and a null engine is rejected up front, for both resume +/// entry points. +#[test] +fn resume_without_pause_is_wrong_state() { + let _guard = ALLOC_LOCK.lock().unwrap(); + ensure_global_allocator(); + + let store = new_store(1024, 1, 256); + let alloc = new_guest_allocator(); + let _idx = load_module_onto(alloc, store, c"main", ADD_WASM, 0).expect("load"); + + // The engine is idle (nothing paused), so both resume paths reject with + // WRONG_STATE rather than corrupting the operand stack. + assert_eq!( + unsafe { spacewasm_resume(store) }, + status::SPACEWASM_ERR_WRONG_STATE, + "resume while not paused" + ); + assert_eq!( + unsafe { spacewasm_resume_value(store, i32_val(0)) }, + status::SPACEWASM_ERR_WRONG_STATE, + "resume_value while not paused" + ); + + // A null engine is rejected before any state inspection. + assert_eq!( + unsafe { spacewasm_resume(core::ptr::null_mut()) }, + status::SPACEWASM_ERR_NULL_ARG, + "resume null engine" + ); + assert_eq!( + unsafe { spacewasm_resume_value(core::ptr::null_mut(), i32_val(0)) }, + status::SPACEWASM_ERR_NULL_ARG, + "resume_value null engine" + ); + + unsafe { + spacewasm_destroy(store); + spacewasm_allocator_destroy(alloc); + } +} + +/// Exercise the `out_trap` output path of `spacewasm_run`: it is cleared up +/// front even on an early return, overwritten with the real trap reason on a +/// trap, and a null `out_trap` is accepted. +#[test] +fn run_out_trap_output_path() { + let _guard = ALLOC_LOCK.lock().unwrap(); + ensure_global_allocator(); + + let store = new_store(1024, 1, 256); + let alloc = new_guest_allocator(); + let idx = load_module_onto(alloc, store, c"main", TRAP_WASM, 0).expect("load"); + let mut func = 0u32; + assert_eq!( + unsafe { spacewasm_find_export_func(store, idx, c"boom".as_ptr(), &mut func) }, + status::SPACEWASM_OK, + "find boom" + ); + + // `run` clears `out_trap` before doing anything, even on the early return + // taken when idle: a stale seed must be reset to NONE. + let mut trap = spacewasm_trap_t::SPACEWASM_TRAP_UNREACHABLE; // stale seed + assert_eq!( + unsafe { spacewasm_run(store, 0, &mut trap) }, + spacewasm_run_status_t::SPACEWASM_RUN_TRAP, + "run while idle traps" + ); + assert_eq!( + trap, + spacewasm_trap_t::SPACEWASM_TRAP_NONE, + "idle run clears out_trap to NONE" + ); + + // A real trap overwrites the seeded value with the actual trap reason. + assert_eq!( + unsafe { spacewasm_invoke(store, idx, func, core::ptr::null(), 0) }, + status::SPACEWASM_OK, + "invoke boom" + ); + trap = spacewasm_trap_t::SPACEWASM_TRAP_HOST; // stale seed + assert_eq!( + run_to_completion(store, &mut trap), + spacewasm_run_status_t::SPACEWASM_RUN_TRAP, + "boom traps" + ); + assert_eq!( + trap, + spacewasm_trap_t::SPACEWASM_TRAP_UNREACHABLE, + "out_trap carries the real trap reason" + ); + + // A null `out_trap` is accepted: after a reset the same call traps again + // with nowhere to report the reason, and must not crash. + assert_eq!( + unsafe { spacewasm_reset(store) }, + status::SPACEWASM_OK, + "reset" + ); + assert_eq!( + unsafe { spacewasm_invoke(store, idx, func, core::ptr::null(), 0) }, + status::SPACEWASM_OK, + "re-invoke boom" + ); + let mut rs = spacewasm_run_status_t::SPACEWASM_RUN_OUT_OF_FUEL; + while rs == spacewasm_run_status_t::SPACEWASM_RUN_OUT_OF_FUEL { + rs = unsafe { spacewasm_run(store, 10000, core::ptr::null_mut()) }; + } + assert_eq!( + rs, + spacewasm_run_status_t::SPACEWASM_RUN_TRAP, + "null out_trap still reports TRAP" + ); + + unsafe { + spacewasm_destroy(store); + spacewasm_allocator_destroy(alloc); + } +} + /// Host implementation of `env.sink`: a void host function (no result type). It /// asserts it received exactly one argument and returns `SPACEWASM_CONTINUE_NONE` /// *while still writing to `out`*, to prove the interpreter honours the "no @@ -2620,17 +2728,18 @@ fn add_host_function_signature_errors() { // Helper to register a function with the given signatures, mapping each // distinct HostFunctionError variant to its FFI status code. - let mut add = |name: &core::ffi::CStr, params: &core::ffi::CStr, returns: &core::ffi::CStr| { - spacewasm_add_host_function( - host.as_mut_ptr(), - hmod, - name.as_ptr(), - params.as_ptr(), - returns.as_ptr(), - Some(add_one), - core::ptr::null_mut(), - ) - }; + let mut add = + |name: &core::ffi::CStr, params: &core::ffi::CStr, returns: &core::ffi::CStr| { + spacewasm_add_host_function( + host.as_mut_ptr(), + hmod, + name.as_ptr(), + params.as_ptr(), + returns.as_ptr(), + Some(add_one), + core::ptr::null_mut(), + ) + }; // Invalid value-list character in the parameter signature. assert_eq!( @@ -2709,15 +2818,15 @@ fn allocator_new_rejects_null_callbacks() { #[test] fn set_global_allocator_rejects_null() { let _guard = ALLOC_LOCK.lock().unwrap(); - // A null callback is rejected with a non-zero code, leaving any previously - // installed allocator in place. + // A null callback is rejected with the "null callback" code, leaving any + // previously installed allocator in place. assert_eq!( crate::spacewasm_set_global_allocator(None, Some(global_dealloc), core::ptr::null_mut()), - 1 + crate::spacewasm_status_t::SPACEWASM_ERR_BAD_ARG ); assert_eq!( crate::spacewasm_set_global_allocator(Some(global_alloc), None, core::ptr::null_mut()), - 1 + crate::spacewasm_status_t::SPACEWASM_ERR_BAD_ARG ); // Re-establish the valid allocator for any subsequent tests. ensure_global_allocator(); @@ -2812,25 +2921,6 @@ fn engine_rejects_out_of_range_module() { } } -/// Create and destroy many stores; the tracked live-byte total must return to -/// its baseline, validating drop order and that names/closures are freed. -#[test] -fn no_leak_across_lifecycle() { - let _guard = ALLOC_LOCK.lock().unwrap(); - ensure_global_allocator(); - - run_add_once(); // absorb one-time allocations - let baseline = crate::spacewasm_memory_statistics().total_bytes; - for _ in 0..50 { - run_add_once(); - } - let after = crate::spacewasm_memory_statistics().total_bytes; - assert_eq!( - after, baseline, - "memory drifted: baseline={baseline} after={after}" - ); -} - /// `(module (import "env" "pause") (func (export "test_pause") (result i32) /// (call 0) (i32.const 42)))` — calls pause, then returns 42 after resume. #[rustfmt::skip] diff --git a/crates/spacewasm_c_api/src/value.rs b/crates/spacewasm_c_api/src/value.rs index b06c73d..759e056 100644 --- a/crates/spacewasm_c_api/src/value.rs +++ b/crates/spacewasm_c_api/src/value.rs @@ -12,6 +12,25 @@ pub enum spacewasm_valtype_t { SPACEWASM_F64 = 3, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InvalidValtypeTag; + +impl TryFrom<&spacewasm_valtype_t> for ValType { + type Error = InvalidValtypeTag; + + fn try_from(tag: &spacewasm_valtype_t) -> Result { + // SAFETY: `spacewasm_valtype_t` is `#[repr(u8)]` + let raw = unsafe { *(tag as *const spacewasm_valtype_t).cast::() }; + match raw { + 0 => Ok(ValType::I32), + 1 => Ok(ValType::I64), + 2 => Ok(ValType::F32), + 3 => Ok(ValType::F64), + _ => Err(InvalidValtypeTag), + } + } +} + impl From for spacewasm_valtype_t { fn from(v: ValType) -> Self { match v { @@ -23,17 +42,6 @@ impl From for spacewasm_valtype_t { } } -impl From for ValType { - fn from(v: spacewasm_valtype_t) -> Self { - match v { - spacewasm_valtype_t::SPACEWASM_I32 => ValType::I32, - spacewasm_valtype_t::SPACEWASM_I64 => ValType::I64, - spacewasm_valtype_t::SPACEWASM_F32 => ValType::F32, - spacewasm_valtype_t::SPACEWASM_F64 => ValType::F64, - } - } -} - /// FFI-safe union of the four WebAssembly 1.0 value payloads. #[repr(C)] #[derive(Clone, Copy)] @@ -52,20 +60,6 @@ pub struct spacewasm_value_t { pub u: spacewasm_value_payload_t, } -impl From for Value { - fn from(value: spacewasm_value_t) -> Self { - // SAFETY: reading the union field that the tag designates as active. - unsafe { - match value.tag { - spacewasm_valtype_t::SPACEWASM_I32 => Value::I32(value.u.i32_), - spacewasm_valtype_t::SPACEWASM_I64 => Value::I64(value.u.i64_), - spacewasm_valtype_t::SPACEWASM_F32 => Value::F32(value.u.f32_), - spacewasm_valtype_t::SPACEWASM_F64 => Value::F64(value.u.f64_), - } - } - } -} - impl From for spacewasm_value_t { fn from(v: Value) -> Self { match v { @@ -90,8 +84,21 @@ impl From for spacewasm_value_t { } impl spacewasm_value_t { - pub fn to_value(self) -> Value { - self.into() + /// Validated conversion of a value received from C into a core [`Value`]. + /// + /// Returns `None` when `tag` is not valid + pub fn try_to_value(&self) -> Option { + let ty = ValType::try_from(&self.tag).ok()?; + // SAFETY: the union field read is selected by the validated tag; every + // bit pattern is a valid value of the corresponding scalar type. + Some(unsafe { + match ty { + ValType::I32 => Value::I32(self.u.i32_), + ValType::I64 => Value::I64(self.u.i64_), + ValType::F32 => Value::F32(self.u.f32_), + ValType::F64 => Value::F64(self.u.f64_), + } + }) } pub fn from_value(v: Value) -> spacewasm_value_t { diff --git a/crates/spacewasm_c_example/examples/ctest_suite.c b/crates/spacewasm_c_example/examples/ctest_suite.c index 11a56d7..f180b8f 100644 --- a/crates/spacewasm_c_example/examples/ctest_suite.c +++ b/crates/spacewasm_c_example/examples/ctest_suite.c @@ -505,7 +505,7 @@ static int test_globals(void) { CHECK(out.u.i32_ == 5, "get_global observes set_g = %d", out.u.i32_); /* Writing a const global is rejected and leaves it unchanged. */ - CHECK(spacewasm_set_global(store, mod_idx, c, i32_val(1)) == SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE, + CHECK(spacewasm_set_global(store, mod_idx, c, i32_val(1)) == SPACEWASM_ERR_GLOBAL_NOT_MUTABLE, "set const"); CHECK(spacewasm_get_global(store, mod_idx, c, &out) == SPACEWASM_OK, "get c after reject"); CHECK(out.u.i32_ == 42, "c unchanged = %d", out.u.i32_); @@ -666,12 +666,15 @@ static int test_globals_imported(void) { } static int test_error_paths(void) { - /* max_modules > 256 -> store_new returns ERR_BAD_ARG (consumes the host). */ + /* max_modules > 256 */ spacewasm_host_t host; CHECK(spacewasm_host_new(0, &host) == SPACEWASM_OK, "host_new"); spacewasm_t* store = NULL; - CHECK(spacewasm_new(&host, 1024, 257, opts(256), &store) == SPACEWASM_ERR_BAD_ARG, - "oversized max_modules"); + spacewasm_status_t bad_arg_st = spacewasm_new(&host, 1024, 257, opts(256), &store); + if (bad_arg_st == SPACEWASM_ERR_BAD_ARG) { + spacewasm_host_destroy(&host); + } + CHECK(bad_arg_st == SPACEWASM_ERR_BAD_ARG, "oversized max_modules"); /* Host function signature errors each map to a distinct status, no panic. */ CHECK(spacewasm_host_new(1, &host) == SPACEWASM_OK, "host_new"); @@ -858,9 +861,10 @@ static int test_pause_and_resume_no_value(void) { /* Resume without value */ CHECK(spacewasm_resume(store) == SPACEWASM_OK, "resume"); - /* Continue running to completion */ - while (spacewasm_run(store, 10000, &trap) == SPACEWASM_RUN_OUT_OF_FUEL) - ; + /* Continue running to completion, capturing the terminal status. */ + spacewasm_run_status_t final_status = run_to_completion(store, &trap); + CHECK(final_status == SPACEWASM_RUN_FINISHED, "run finished (status=%d, trap=%d)", + (int)final_status, (int)trap); CHECK(trap == SPACEWASM_TRAP_NONE, "no trap"); /* Check result */ @@ -915,9 +919,10 @@ static int test_pause_and_resume_with_value(void) { spacewasm_value_t resume_val = {SPACEWASM_I32, {.i32_ = 99}}; CHECK(spacewasm_resume_value(store, resume_val) == SPACEWASM_OK, "resume_value"); - /* Continue running to completion */ - while (spacewasm_run(store, 10000, &trap) == SPACEWASM_RUN_OUT_OF_FUEL) - ; + /* Continue running to completion, capturing the terminal status. */ + spacewasm_run_status_t final_status = run_to_completion(store, &trap); + CHECK(final_status == SPACEWASM_RUN_FINISHED, "run finished (status=%d, trap=%d)", + (int)final_status, (int)trap); CHECK(trap == SPACEWASM_TRAP_NONE, "no trap"); /* Check result - should be the resumed value (99) */ @@ -933,7 +938,7 @@ static int test_pause_and_resume_with_value(void) { /* ---- runner -------------------------------------------------------------- */ int main(void) { - if (spacewasm_set_global_allocator(heap_alloc, heap_dealloc, NULL) != 0) { + if (spacewasm_set_global_allocator(heap_alloc, heap_dealloc, NULL) != SPACEWASM_OK) { fprintf(stderr, "set_global_allocator failed\n"); return 1; } diff --git a/crates/spacewasm_std/benches/coremark.rs b/crates/spacewasm_std/benches/coremark.rs index 22a3bca..e476584 100644 --- a/crates/spacewasm_std/benches/coremark.rs +++ b/crates/spacewasm_std/benches/coremark.rs @@ -51,7 +51,7 @@ fn main() { let mut state = Engine::new(1024, 2, WasmVec::from_array([env]).unwrap()).unwrap(); let mut code_builder = CodeBuilder::new(CompilerOptions { allow_memory_grow: false, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_CODE_PAGES, }) .unwrap(); @@ -146,7 +146,6 @@ fn main() { if coremark_score > 1.0 { println!("=== CoreMark Results ==="); println!("CoreMark Score: {:.3}", coremark_score); - println!("CoreMark/MHz: {:.3}", coremark_score); println!( "Iterations/sec: {:.2}", coremark_score as f64 / elapsed.as_secs_f64() diff --git a/crates/spacewasm_std/src/main.rs b/crates/spacewasm_std/src/main.rs index 9a42374..171c7a3 100644 --- a/crates/spacewasm_std/src/main.rs +++ b/crates/spacewasm_std/src/main.rs @@ -16,13 +16,18 @@ const MAX_CODE_PAGES: u32 = 256; const MAX_CONTROL_FRAMES: usize = 64; const MAX_STACK_DEPTH: usize = 256; +fn guest_error(msg: impl core::fmt::Display) -> ! { + eprintln!("error: {msg}"); + std::process::exit(1); +} + fn main() { let path = std::env::args().nth(1).unwrap(); let start = Instant::now(); let mut code_builder = CodeBuilder::new(CompilerOptions { allow_memory_grow: false, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_CODE_PAGES, }) .expect("failed to allocate code builder"); @@ -42,8 +47,14 @@ fn main() { panic!("expected i32"); }; - let f = state.memory.load(*addr as usize, *len as usize).unwrap(); - let s: &str = core::str::from_utf8(f).unwrap(); + // `addr`/`len` are guest-supplied: an out-of-bounds or + // non-UTF-8 pointer must trap the guest, not panic the host CLI. + let Ok(f) = state.memory.load(*addr as usize, *len as usize) else { + return ControlFlow::Break(HostFunctionBreak::Trap); + }; + let Ok(s) = core::str::from_utf8(f) else { + return ControlFlow::Break(HostFunctionBreak::Trap); + }; eprintln!("PANIC {}:{}", s, line_no); ControlFlow::Break(HostFunctionBreak::Trap) @@ -64,12 +75,12 @@ fn main() { panic!("expected i32"); }; - let msg_r = state - .memory - .load(*msg_ptr as usize, *msg_len as usize) - .unwrap(); - - let msg: &str = core::str::from_utf8(msg_r).unwrap(); + let Ok(msg_r) = state.memory.load(*msg_ptr as usize, *msg_len as usize) else { + return ControlFlow::Break(HostFunctionBreak::Trap); + }; + let Ok(msg) = core::str::from_utf8(msg_r) else { + return ControlFlow::Break(HostFunctionBreak::Trap); + }; eprintln!("MESSAGE {msg}"); ControlFlow::Continue(None) @@ -91,17 +102,17 @@ fn main() { panic!("expected i32"); }; - // Time base - state.memory.store_u16(*time_ptr as usize, 0).unwrap(); - - // Time context - state.memory.store_u8((*time_ptr as usize) + 2, 0).unwrap(); - - // Seconds - state.memory.store_u32((*time_ptr as usize) + 3, 0).unwrap(); - - // Useconds - state.memory.store_u32((*time_ptr as usize) + 7, 0).unwrap(); + // `time_ptr` is guest-supplied: trap on any out-of-bounds store + // rather than panicking the host CLI. + let wrote = state + .memory + .store_u16(*time_ptr as usize, 0) // Time base + .and(state.memory.store_u8((*time_ptr as usize) + 2, 0)) // Time context + .and(state.memory.store_u32((*time_ptr as usize) + 3, 0)) // Seconds + .and(state.memory.store_u32((*time_ptr as usize) + 7, 0)); // Useconds + if wrote.is_err() { + return ControlFlow::Break(HostFunctionBreak::Trap); + } eprintln!("TELEMETRY {id}"); ControlFlow::Continue(Some(Value::I32(0))) @@ -138,7 +149,7 @@ fn main() { let file = std::fs::File::open(path).expect("failed to open file"); let mut file_stream = FileStream::new(file); let (module, stats) = - spacewasm::Module::new_with_statistics::( + match spacewasm::Module::new_with_statistics::( "main", &mut file_stream, &mut state.store, @@ -146,20 +157,27 @@ fn main() { spacewasm::Rc::new(RustSystemAllocator) .unwrap() .into_wasm_memory_allocator(), - ) - .expect("failed to parse wasm module"); + ) { + Ok(parsed) => parsed, + Err(e) => guest_error(format!("failed to decode/validate wasm module: {e:?}")), + }; let text = code_builder.pages(); let final_page_offset = code_builder.offset(); - let module_ref = state.push_module(module).unwrap(); + let module_ref = match state.push_module(module) { + Ok(module_ref) => module_ref, + Err(e) => guest_error(format!("failed to instantiate wasm module: {e:?}")), + }; if let Some(start) = state.module_start(module_ref) { - state.invoke(start, &[]).unwrap(); + if let Err(e) = state.invoke(start, &[]) { + guest_error(format!("failed to invoke start function: {e:?}")); + } match spacewasm::Interpreter.run(text, &mut state, usize::MAX) { InterpreterResult::Finished => {} - InterpreterResult::OutOfFuel => panic!("insufficient fuel for initialization"), - InterpreterResult::Trap(t) => panic!("trap during initialization {t:?}"), - InterpreterResult::Pause => panic!("pause during init"), + InterpreterResult::OutOfFuel => guest_error("insufficient fuel for initialization"), + InterpreterResult::Trap(t) => guest_error(format!("trap during initialization: {t:?}")), + InterpreterResult::Pause => guest_error("unexpected pause during initialization"), } } @@ -215,27 +233,29 @@ fn main() { let module = state.store.modules().last().unwrap(); let fi = { - let f = module.exports.iter().find(|f| &f.name == "run").unwrap(); + let Some(f) = module.exports.iter().find(|f| &f.name == "run") else { + guest_error("wasm module does not export a `run` function"); + }; let ExportDesc::Func(fi) = f.desc else { - panic!() + guest_error("exported `run` is not a function"); }; fi }; let module = state.store.modules().last().unwrap(); - let Ref::Module(fi) = module.get_func_ref(fi).unwrap() else { - panic!() + let Some(Ref::Module(fi)) = module.get_func_ref(fi) else { + guest_error("exported `run` function reference is invalid"); }; - state - .invoke( - WasmRef { - module: ModuleRef(0), - index: fi, - }, - &[], - ) - .unwrap(); + if let Err(e) = state.invoke( + WasmRef { + module: ModuleRef(0), + index: fi, + }, + &[], + ) { + guest_error(format!("failed to invoke `run` function: {e:?}")); + } let mut result = InterpreterResult::OutOfFuel; while result == InterpreterResult::OutOfFuel { @@ -243,7 +263,7 @@ fn main() { } let InterpreterResult::Finished = result else { - panic!("interpreter failed: {:?}", result) + guest_error(format!("interpreter failed: {result:?}")); }; eprintln!( diff --git a/crates/spacewasm_util/src/bin/spacewasm-trace.rs b/crates/spacewasm_util/src/bin/spacewasm-trace.rs index 8b969c9..91b95c6 100644 --- a/crates/spacewasm_util/src/bin/spacewasm-trace.rs +++ b/crates/spacewasm_util/src/bin/spacewasm-trace.rs @@ -9,8 +9,8 @@ use spacewasm::{ AllocError, Allocator, CodeBuilder, CompilerOptions, Engine, ExportDesc, InnerVec, Interpreter, - InterpreterResult, InterpreterRunner, InvokeError, MemoryStatistics, Module, ModuleRef, Ref, - TrapReason, Vec as WasmVec, WasmMemoryAllocator, WasmRef, WasmStream, + InterpreterResult, InterpreterRunner, InvokeError, Module, ModuleRef, Ref, TrapReason, + Vec as WasmVec, WasmMemoryAllocator, WasmRef, WasmStream, }; use spacewasm::{ValType, Value}; use spacewasm_util::StateTracer; @@ -45,10 +45,12 @@ impl WasmStream for ByteStream { } self.consumed = true; - let inner = InnerVec { - ptr: self.buffer.as_mut_ptr(), - capacity: self.buffer.len() as u32, - len: self.buffer.len() as u32, + let inner = unsafe { + InnerVec::from_raw_parts( + self.buffer.as_mut_ptr(), + self.buffer.len() as u32, + self.buffer.len() as u32, + ) }; Ok(Some(inner)) } @@ -77,13 +79,6 @@ unsafe impl Allocator for SystemAllocator { unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) { unsafe { std::alloc::dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: 0, - pad_bytes: 0, - } - } } impl WasmMemoryAllocator for SystemAllocator { @@ -197,7 +192,7 @@ fn main() { // Compile module let mut code_builder = CodeBuilder::new(CompilerOptions { allow_memory_grow: true, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_CODE_PAGES, }) .unwrap(); diff --git a/crates/spacewasm_util/src/file.rs b/crates/spacewasm_util/src/file.rs index f1d86a0..d8af9d9 100644 --- a/crates/spacewasm_util/src/file.rs +++ b/crates/spacewasm_util/src/file.rs @@ -2,6 +2,29 @@ use spacewasm::{InnerVec, WasmStream}; use std::collections::{HashMap, VecDeque}; use std::io::Read; +/// Number of reusable read buffers held by a [`FileStream`]'s pool. +const BUFFER_POOL_SIZE: usize = 8; + +/// Size, in bytes, of each buffer in a [`FileStream`]'s pool. +const BUFFER_SIZE: usize = 1024; + +/// Error code reported by [`FileStream::read`] when the underlying I/O error has +/// no OS errno that can be represented in the stream's single-`u8` error +/// channel. +const UNKNOWN_IO_ERROR: u8 = 0xFF; + +/// Map an OS errno (as returned by [`std::io::Error::raw_os_error`]) into the +/// stream's single-`u8` error channel, applying the [`UNKNOWN_IO_ERROR`] +/// encoding documented on that constant. Only errnos in `1..=254` are passed +/// through verbatim; everything else (missing errno, `0`, or a value that would +/// truncate or collide with the sentinel) is reported as [`UNKNOWN_IO_ERROR`]. +fn errno_to_code(errno: Option) -> u8 { + match errno { + Some(e) if (1..UNKNOWN_IO_ERROR as i32).contains(&e) => e as u8, + _ => UNKNOWN_IO_ERROR, + } +} + pub struct FileStream { file: std::fs::File, ready: VecDeque>, @@ -12,8 +35,8 @@ pub struct FileStream { impl FileStream { pub fn new(file: std::fs::File) -> FileStream { let mut ready = VecDeque::new(); - for _ in 0..8 { - ready.push_back(vec![0u8; 1024]); + for _ in 0..BUFFER_POOL_SIZE { + ready.push_back(vec![0u8; BUFFER_SIZE]); } FileStream { @@ -35,20 +58,23 @@ impl FileStream { impl WasmStream for FileStream { fn read(&mut self) -> Result>, u8> { - let mut buf = self.ready.pop_front().expect("no more buffers"); + let mut buf = self.ready.pop_front().unwrap_or_else(|| { + panic!( + "FileStream buffer pool exhausted: all {BUFFER_POOL_SIZE} buffers \ + are checked out; return chunks with `return_` before reading again" + ) + }); let n = self.file.read(&mut buf).map_err(|err| { eprintln!("Failed to read file: {}", err); - err.raw_os_error().unwrap_or(0) as u8 + errno_to_code(err.raw_os_error()) })?; if n == 0 { Ok(None) } else { - let m = InnerVec { - ptr: buf.as_mut_ptr(), - capacity: buf.capacity() as u32, - len: n as u32, + let m = unsafe { + InnerVec::from_raw_parts(buf.as_mut_ptr(), buf.capacity() as u32, n as u32) }; self.n += n; @@ -58,7 +84,49 @@ impl WasmStream for FileStream { } fn return_(&mut self, chunk: InnerVec) { - let buf = self.used.remove(&chunk.ptr).unwrap(); + let buf = self.used.remove(&chunk.ptr()).unwrap(); self.ready.push_back(buf); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn errno_in_range_is_passed_through() { + assert_eq!(errno_to_code(Some(1)), 1); + assert_eq!(errno_to_code(Some(2)), 2); + assert_eq!(errno_to_code(Some(13)), 13); + assert_eq!(errno_to_code(Some(254)), 254); + } + + #[test] + fn missing_errno_maps_to_sentinel() { + assert_eq!(errno_to_code(None), UNKNOWN_IO_ERROR); + } + + #[test] + fn zero_errno_maps_to_sentinel() { + // `0` is reserved to mean "no error"; it must never be emitted as an + // error code. + assert_eq!(errno_to_code(Some(0)), UNKNOWN_IO_ERROR); + } + + #[test] + fn out_of_range_errno_maps_to_sentinel_without_colliding() { + // Values that would truncate into a colliding byte are remapped to the + // sentinel rather than silently wrapping. + assert_eq!(errno_to_code(Some(255)), UNKNOWN_IO_ERROR); // equals sentinel + assert_eq!(errno_to_code(Some(256)), UNKNOWN_IO_ERROR); // would truncate to 0 + assert_eq!(errno_to_code(Some(511)), UNKNOWN_IO_ERROR); // would truncate to 255 + assert_eq!(errno_to_code(Some(-1)), UNKNOWN_IO_ERROR); + } + + #[test] + fn sentinel_is_nonzero() { + // A nonzero sentinel keeps `0` free for "no error" and guarantees the + // passthrough range never produces it. + assert_ne!(UNKNOWN_IO_ERROR, 0); + } +} diff --git a/crates/spacewasm_util/src/lib.rs b/crates/spacewasm_util/src/lib.rs index 21a83ce..ce35e46 100644 --- a/crates/spacewasm_util/src/lib.rs +++ b/crates/spacewasm_util/src/lib.rs @@ -1,4 +1,4 @@ -use spacewasm::{AllocError, Allocator, MemoryStatistics, WasmMemoryAllocator}; +use spacewasm::{AllocError, Allocator, WasmMemoryAllocator}; use std::alloc::Layout; use std::ptr::NonNull; @@ -23,10 +23,6 @@ unsafe impl Allocator for RustSystemAllocator { unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { std::alloc::dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> MemoryStatistics { - panic!("The page allocator should be tracking it's own memory statistics.") - } } impl WasmMemoryAllocator for RustSystemAllocator { diff --git a/docs/ir.md b/docs/ir.md index 2ba1825..3981c58 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -88,6 +88,8 @@ datastructures within the implementation. - **Host modules**: Maximum 256 host modules - **Function parameters**: Maximum 255 32-bit words - **Local variables**: Maximum 65,535 32-bit words total per function +- **Import names**: Each import's module name and field name are limited to 32 bytes; a longer name is rejected at decode time +- **Custom-section names**: Limited to 32 bytes; a longer name is rejected at decode time ### Linear Memory diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 88c12ad..da7b129 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,7 +8,7 @@ edition = "2024" cargo-fuzz = true [dependencies] -libfuzzer-sys = "0.4" +libfuzzer-sys = "=0.4.13" spacewasm = { path = "..", features = ["strict-assertions"] } spacewasm-fuzzing = { path = "../crates/fuzzing", features = ["differential"] } diff --git a/src/code.rs b/src/code.rs index f570eb2..f1d0de7 100644 --- a/src/code.rs +++ b/src/code.rs @@ -96,34 +96,38 @@ impl Module { let empty_f = self.functions[i].clone(); let mut f = core::mem::replace(&mut self.functions[i], empty_f); - f.locals = wasm.read_vec(|w| { - let n = w.read_u32()?; - let t = ValType::read(w)?; + wasm.with_limit(size as usize, |wasm| { + f.locals = wasm.read_vec(|w| { + let n = w.read_u32()?; + let t = ValType::read(w)?; - if n > 0xFFFF { - return Err(ValidationError::TooManyLocals); - } + if n > 0xFFFF { + return Err(ValidationError::TooManyLocals); + } - Ok((n as u16, t)) - })?; + Ok((n as u16, t)) + })?; - // Compute the local size in words - let mut total_size: usize = 0; - for (n, ty) in f.locals.iter() { - total_size = (*n as usize) - .checked_mul(ty.size()) - .and_then(|v| total_size.checked_add(v)) - .ok_or(ValidationError::TooManyLocals)?; - } - let size_in_words = total_size / 4; + // Compute the local size in words + let mut total_size: usize = 0; + for (n, ty) in f.locals.iter() { + total_size = (*n as usize) + .checked_mul(ty.size()) + .and_then(|v| total_size.checked_add(v)) + .ok_or(ValidationError::TooManyLocals)?; + } + let size_in_words = total_size / 4; - if size_in_words > 0xFFFF { - return Err(ValidationError::TooManyLocals); - } + if size_in_words > 0xFFFF { + return Err(ValidationError::TooManyLocals); + } - f.local_size = size_in_words as u16; - (f.expr, f.stack_usage) = - Expr::read::(wasm, builder, store, self, &f)?; + f.local_size = size_in_words as u16; + (f.expr, f.stack_usage) = + Expr::read::(wasm, builder, store, self, &f)?; + + Ok(()) + })?; // Bound the worst-case call frame this function can produce. When a // function performs a call, the frame it leaves behind is diff --git a/src/compiler.rs b/src/compiler.rs index c63f8ae..ef92003 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -337,7 +337,7 @@ impl<'a, const MAX_CONTROL_FRAMES: usize, const MAX_STACK_DEPTH: usize> WasmVisi fn global_set(&self, x: GlobalIdx, state: &mut Self::State) -> Result<(), Self::Error> { let g = state.get_global(x)?; if !g.mutable { - Err(ValidationError::GlobalIsNotMutable) + Err(ValidationError::GlobalNotMutable) } else { let _ = state.pop_stack(g.ty)?; match g.reference { diff --git a/src/constant.rs b/src/constant.rs index 79d2bde..49c39f2 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -294,21 +294,25 @@ impl<'a> WasmVisitor for ConstantCompiler<'a> { .get_global_ref(x) .ok_or(ConstantExprError::InvalidGlobal)? { - Ref::Module(idx) => self - .module - .globals - .get(idx as usize) - .ok_or(ConstantExprError::InvalidGlobal)? - .value(), + // Non-imported (module-local) global: not allowed in a const-expr. + Ref::Module(_) => return Err(ConstantExprError::InvalidConstantInstruction), Ref::Host { module, index } => { // Look up the host module/global and read its value - self.store + let global = self + .store .host_modules() .get(module.0 as usize) .ok_or(ConstantExprError::InvalidGlobal)? .globals .get(index as usize) - .ok_or(ConstantExprError::InvalidGlobal)? + .ok_or(ConstantExprError::InvalidGlobal)?; + + // Mutable globals cannot appear in a constant expression. + if global.value.mutable() { + return Err(ConstantExprError::InvalidConstantInstruction); + } + + global .value .read() .ok() @@ -316,14 +320,21 @@ impl<'a> WasmVisitor for ConstantCompiler<'a> { } Ref::Extern { module, index } => { // Look up the external wasm module and read the globals value - self.store + let global = self + .store .modules() .get(module.0 as usize) .ok_or(ConstantExprError::InvalidGlobal)? .globals .get(index as usize) - .ok_or(ConstantExprError::InvalidGlobal)? - .value() + .ok_or(ConstantExprError::InvalidGlobal)?; + + // Mutable globals cannot appear in a constant expression. + if global.type_.mutable { + return Err(ConstantExprError::InvalidConstantInstruction); + } + + global.value() } }; diff --git a/src/error.rs b/src/error.rs index 019cb64..2bdec97 100644 --- a/src/error.rs +++ b/src/error.rs @@ -75,6 +75,7 @@ pub enum ValidationError { LabelJumpTooLarge, TypeMismatch, BlockResultTypeMismatch, + BrTableResultTypeMismatch, FunctionResultTypeMismatch, IllegalMemoryGrow, InvalidElementOffset, @@ -106,7 +107,7 @@ pub enum ValidationError { TableImportNotFound, FunctionImportOutOfRange, FunctionImportTypeMismatch, - GlobalIsNotMutable, + GlobalNotMutable, GlobalImportTypeMismatch, MemoryImportTypeMismatch, TableImportTypeMismatch, diff --git a/src/host.rs b/src/host.rs index ab677cc..1cc8949 100644 --- a/src/host.rs +++ b/src/host.rs @@ -330,7 +330,7 @@ pub struct HostFunction { name: HostName, params: HostValList, returns: ResultType, - f: HostFunctionFn, + f: Option, } impl Debug for HostFunction { @@ -369,6 +369,14 @@ impl HostFunction { /// via the panicking [`HostName::new`] / [`HostValList::new`] constructors, /// so this is only appropriate for compile-time-known values in Rust code. /// FFI callers should validate input and use [`HostFunction::try_new`]. + /// + /// # Panics + /// + /// Panics if the parameter/result signature is too large or contains + /// multiple returns, or if the closure allocation fails + /// ([`HostFunctionError::AllocError`]). [`HostFunction::try_new`] routes all + /// of these through a `Result` instead of panicking; prefer it on + /// caller-supplied input. pub fn new( name: impl Into>, params: HostValList, @@ -392,17 +400,8 @@ impl HostFunction { return Err(HostFunctionError::ParameterListTooLong); } - // A sanity check to make sure that the parameters fit within the IR supported - // frame size. This check is unwrapped because the `MAX_HOST_FUNCTION_PARAMS` should - // already guard against this check (i.e. the parameter length also bounds the param size). - let ps = params - .iter() - .fold(0, |n: usize, i| n.checked_add(i.size()).unwrap()) - / 4; - - if ps > 0xFFFF { - unreachable!() - } + // Make sure the max host functions (even if all i64) fit in the IR + const { assert!(MAX_HOST_FUNCTION_PARAMS.saturating_mul(2) <= 0xFFFF) }; let mut rs: Option = None; for r in returns.iter() { @@ -417,7 +416,7 @@ impl HostFunction { name, params, returns: ResultType(rs), - f: Box::new(f)?.into_host_function_dyn(), + f: Some(Box::new(f)?.into_host_function_dyn()), }) } @@ -433,15 +432,29 @@ impl HostFunction { self.params.iter().fold(0, |n, i| n + i.size()) / 4 } - pub fn get_call(&mut self) -> HostFunctionFn { - core::mem::replace( - &mut self.f, - Box::new(placeholder).unwrap().into_host_function_dyn(), - ) - } - + /// Move the host closure out for the duration of a single call. + /// + /// The closure must be taken out of `self` because invoking it requires a + /// `&mut Engine` that mutably borrows the store which owns this + /// `HostFunction`; leaving `None` behind avoids that borrow conflict without + /// allocating a placeholder. The caller MUST return the closure via + /// [`HostFunction::finish_call`] before this function can be reached again. + /// + /// # Reentrancy + /// + /// The engine is single-threaded and a host function is not permitted to + /// reenter itself (directly or transitively). If it does, the closure has + /// already been taken and this returns `None`. + pub fn get_call(&mut self) -> Option { + self.f.take() + } + + /// Restore the closure previously taken by [`HostFunction::get_call`] once + /// the call has returned. Must be paired with exactly one preceding + /// `get_call`; the closure returns to the always-`Some` steady state. pub fn finish_call(&mut self, f: HostFunctionFn) { - let _ = core::mem::replace(&mut self.f, f); + let other = self.f.replace(f); + debug_assert!(other.is_none()); } pub fn name(&self) -> &str { @@ -449,6 +462,36 @@ impl HostFunction { } } -fn placeholder(_: &mut Engine, _: &[Value]) -> HostFunctionResult { - panic!("invoked invalid host module") +#[cfg(test)] +mod tests { + use super::*; + + fn dummy() -> HostFunction { + HostFunction::new( + "dummy", + HostValList::new("ii"), + HostValList::new(""), + |_, _| ControlFlow::Continue(None), + ) + } + + // `get_call` must move the closure out (no per-call allocation) and + // `finish_call` must restore it. A second `get_call` while the closure is + // out models a reentrant invocation and must observe `None` rather than + // panicking (findings: non-allocating get_call + reentrancy-as-trap). + #[test] + fn get_call_takes_and_finish_call_restores() { + let mut f = dummy(); + + // Steady state: the closure is present. + let taken = f.get_call(); + assert!(taken.is_some()); + + // While the closure is out, a reentrant `get_call` observes `None`. + assert!(f.get_call().is_none()); + + // Restoring returns to the always-`Some` steady state. + f.finish_call(taken.unwrap()); + assert!(f.get_call().is_some()); + } } diff --git a/src/imports.rs b/src/imports.rs index 786f5ea..5bc8e28 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -146,7 +146,7 @@ impl Store { return if g.value.ty() != expected_ty.ty { Err(ValidationError::GlobalImportTypeMismatch) } else if g.value.mutable() != expected_ty.mutable { - Err(ValidationError::GlobalIsNotMutable) + Err(ValidationError::GlobalNotMutable) } else { Ok(Import::HostGlobal { module: HostModuleRef::new(mi), @@ -176,7 +176,7 @@ impl Store { if g.type_.ty != expected_ty.ty { Err(ValidationError::GlobalImportTypeMismatch) } else if expected_ty.mutable != g.type_.mutable { - Err(ValidationError::GlobalIsNotMutable) + Err(ValidationError::GlobalNotMutable) } else { Ok(Import::Global { module: ModuleRef(mi as u8), @@ -190,7 +190,7 @@ impl Store { if g.type_.ty != expected_ty.ty { Err(ValidationError::GlobalImportTypeMismatch) } else if expected_ty.mutable != g.type_.mutable { - Err(ValidationError::GlobalIsNotMutable) + Err(ValidationError::GlobalNotMutable) } else { Ok(Import::Global { module, index }) } @@ -201,7 +201,7 @@ impl Store { if g.value.ty() != expected_ty.ty { Err(ValidationError::GlobalImportTypeMismatch) } else if expected_ty.mutable != g.value.mutable() { - Err(ValidationError::GlobalIsNotMutable) + Err(ValidationError::GlobalNotMutable) } else { Ok(Import::HostGlobal { module, index }) } @@ -238,8 +238,6 @@ impl Store { }); } } - - return Err(ValidationError::TableImportNotFound); } } diff --git a/src/interpreter.rs b/src/interpreter.rs index e2c5e1d..14931fe 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -7,20 +7,25 @@ impl LocalVariable { } } +#[repr(C)] struct CallFrame { frame_length: u16, - module_delta: u8, + module_delta: i8, parameter_size: u8, } impl CallFrame { pub fn from_bits(bits: u32) -> CallFrame { - // SAFETY: We are converting from the serialized form. The sizes are checked at compile time. + // SAFETY: `CallFrame` and `u32` have identical size (asserted at compile + // time) and every 32-bit pattern is a valid `CallFrame`. + const { assert!(core::mem::size_of::() == core::mem::size_of::()) }; unsafe { core::mem::transmute(bits) } } - // SAFETY: We are converting to the serialized form. The sizes are checked at compile time. pub fn into_bits(self) -> u32 { + // SAFETY: see `from_bits`; the `#[repr(C)]` layout serializes directly + // to a `u32` and the size equality is asserted at compile time. + const { assert!(core::mem::size_of::() == core::mem::size_of::()) }; unsafe { core::mem::transmute(self) } } } @@ -33,6 +38,21 @@ pub enum InvokeError { ParamTypeMismatch, /// The function requires more stack space than the interpreter has remaining StackOverflow, + /// The engine is not currently idle and therefore we cannot invoke + Busy, +} + +/// Error returned by [`Engine::resume`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResumeError { + /// `resume` was called but the engine is not paused waiting for a host + /// function result. + NotPaused, + /// The supplied resume value's type does not match the result type declared + /// by the paused host function (this also covers a missing or unexpected + /// value). The pending pause is preserved so the caller may retry with a + /// correctly-typed value. + ResultTypeMismatch, } impl Engine { @@ -42,7 +62,7 @@ impl Engine { 0 } else { // Swap to the extern module's context - let delta = f_ref.module.0.wrapping_sub(self.module.0); + let delta = f_ref.module.0.wrapping_sub(self.module.0) as i8; self.module = f_ref.module; self.memory = self.store.get_memory(self.module).clone(); self.table = self.store.get_table(self.module).clone(); @@ -53,7 +73,7 @@ impl Engine { } /// Call a function within the _current_ module - fn call_impl(&mut self, module_delta: u8, index: u16) -> Result<(), InterpreterBreak> { + fn call_impl(&mut self, module_delta: i8, index: u16) -> Result<(), InterpreterBreak> { // Make sure we have enough stack space for the function call let m = &self.store.modules()[self.module.0 as usize]; let f = &m.functions[index as usize]; @@ -99,11 +119,11 @@ impl Engine { /// This function can only be used to kick off the interpreter. /// It cannot be invoked once the interpreter has started. pub fn invoke(&mut self, f_ref: WasmRef, params: &[Value]) -> Result<(), InvokeError> { - // Make sure we are looking at the sentinel program counter - // This is only the case when nothing is running - assert_eq!(self.pc, JumpTarget::SENTINEL); - assert_eq!(self.sp, 0); - assert_eq!(self.fp, 0); + // `invoke` may only kick off execution from a fully idle engine + // We currently do not have a notion of an interrupt. + if self.pc != JumpTarget::SENTINEL || self.sp != 0 || self.fp != 0 { + return Err(InvokeError::Busy); + } let m = &self.store.modules()[f_ref.module.0 as usize]; let f = &m.functions[f_ref.index as usize]; @@ -140,6 +160,7 @@ impl Engine { self.sp += 2; } _ => { + self.sp = 0; return Err(InvokeError::ParamTypeMismatch); } } @@ -149,8 +170,9 @@ impl Engine { self.module = f_ref.module; self.memory = self.store.get_memory(self.module).clone(); self.table = self.store.get_table(self.module).clone(); - self.call_impl(0, f_ref.index) - .map_err(|_| InvokeError::StackOverflow)?; + + // This unwrap should be safe because we checked for stack overflow up-front. + self.call_impl(0, f_ref.index).unwrap(); self.jumped = false; self.result = None; @@ -159,9 +181,21 @@ impl Engine { /// Resume the interpreter after a host pause. /// Optionally pushes a value to the operand stack as a host function return value. - pub fn resume(&mut self, resume_value: Option) { - // Unwrap is safe here because we should not call resume() unless the interpreter requested a pause - match (self.host_pause_result.take().unwrap(), resume_value) { + /// + /// Returns [`ResumeError::NotPaused`] if the engine is not currently paused + /// waiting for a host function result, and [`ResumeError::ResultTypeMismatch`] + /// if `resume_value` does not match the paused host function's declared + /// result type (in which case the pending pause is left intact so the caller + /// may retry). + pub fn resume(&mut self, resume_value: Option) -> Result<(), ResumeError> { + // `resume` is only valid when a host function has paused the engine and + // is awaiting a result. If nothing is pending, report it instead of + // panicking. + let Some(pause_result) = self.host_pause_result.take() else { + return Err(ResumeError::NotPaused); + }; + + match (pause_result, resume_value) { (ResultType(Some(ValType::F32)), Some(Value::F32(z))) => { self.stack.write_f32(self.sp, z); self.sp += 1; @@ -179,8 +213,14 @@ impl Engine { self.sp += 2; } (ResultType(None), None) => {} - _ => panic!("expected host function to return a value"), + _ => { + // Result the pause result ty + self.host_pause_result = Some(pause_result); + return Err(ResumeError::ResultTypeMismatch); + } } + + Ok(()) } } @@ -1357,7 +1397,7 @@ impl IrVisitor for Interpreter { // Check if we are leaving the context of this module if call_frame.module_delta != 0 { // Restore the old module context outside this frame - let restore_module = state.module.0.wrapping_sub(call_frame.module_delta); + let restore_module = state.module.0.wrapping_sub(call_frame.module_delta as u8); state.module = ModuleRef(restore_module); state.memory = state.store.get_memory(state.module).clone(); state.table = state.store.get_table(state.module).clone(); @@ -1734,7 +1774,7 @@ mod kani_proofs { #[kani::proof] fn proof_callframe_transmute_roundtrip() { let frame_length: u16 = kani::any(); - let module_delta: u8 = kani::any(); + let module_delta: i8 = kani::any(); let parameter_size: u8 = kani::any(); let frame = CallFrame { @@ -1765,7 +1805,7 @@ mod kani_proofs { #[kani::proof] fn proof_callframe_deterministic() { let frame_length: u16 = kani::any(); - let module_delta: u8 = kani::any(); + let module_delta: i8 = kani::any(); let parameter_size: u8 = kani::any(); let frame1 = CallFrame { @@ -1808,4 +1848,73 @@ mod kani_proofs { assert_eq!(decoded.module_delta, 0x56); assert_eq!(decoded.parameter_size, 0x78); } + + /// Model the stack-pointer arithmetic of a call-frame push (`call_impl`) + /// followed by the matching return unwind (`return_`), proving that the + /// frame pointer round-trips and that none of the offset computations + /// overflow or underflow under the invariants that hold at the call site. + #[kani::proof] + fn proof_call_return_sp_arithmetic() { + // Symbolic caller state at the call site. `prev_fp` is the caller's + // frame pointer; `sp0` is the stack pointer once the callee's arguments + // have been pushed — this value becomes the callee frame pointer. + let prev_fp: u32 = kani::any(); + let sp0: u32 = kani::any(); + // Word counts drawn from the callee definition. + let parameter_words: u8 = kani::any(); + let local_size: u16 = kani::any(); + let return_size: usize = kani::any(); + // Entry/normal returns move at most a 64-bit (2-word) result. + kani::assume(return_size <= 2); + + // Invariants established by construction and validation: + // - the new frame pointer never precedes the caller frame pointer, + // - the caller pushed `parameter_words` argument words below the fp, + // - the frame span fits the 16-bit `CallFrame::frame_length` field, + // - the stack pointer stays inside the addressable (u32) range so the + // widening/narrowing conversions cannot mask overflow. + kani::assume(prev_fp <= sp0); + kani::assume(sp0 >= parameter_words as u32); + let frame_span = sp0 - prev_fp; + kani::assume(frame_span <= u16::MAX as u32); + kani::assume((sp0 as usize) + 2 + local_size as usize <= u32::MAX as usize); + + // --- call_impl: push the frame and allocate locals --- + let frame = CallFrame { + frame_length: frame_span as u16, + module_delta: 0, + parameter_size: parameter_words, + }; + // The interpreter sets `fp = sp` before allocating the frame + locals. + let fp = sp0; + let sp_after_push = sp0 as usize + 2 + local_size as usize; + assert!( + sp_after_push >= sp0 as usize + 2, + "allocating locals never shrinks the frame" + ); + + // --- return_: unwind the frame --- + let decoded = CallFrame::from_bits(frame.into_bits()); + let return_fp = fp - decoded.frame_length as u32; + assert_eq!( + return_fp, prev_fp, + "return must restore the caller frame pointer" + ); + + // `parameter_start = fp - parameter_size` must not underflow. + let parameter_start = fp as usize - decoded.parameter_size as usize; + assert!( + parameter_start <= fp as usize, + "parameter_start must not underflow past the frame pointer" + ); + + // A non-entry return writes results into + // `[parameter_start, parameter_start + return_size)` and leaves `sp` + // there; verify that stays within the space the push allocated. + let sp_after_return = parameter_start + return_size; + assert!( + sp_after_return <= sp_after_push, + "the return stack pointer stays within the pushed frame" + ); + } } diff --git a/src/interpreter_tests.rs b/src/interpreter_tests.rs index 5ecdbcd..895a953 100644 --- a/src/interpreter_tests.rs +++ b/src/interpreter_tests.rs @@ -4,7 +4,8 @@ mod tests { use crate::{ AllocError, BaseVisitor, Engine, Interpreter, InterpreterResult, InterpreterRunner, - IrVisitor, MemArg, MemType, Memory, MemoryKind, Module, ModuleRef, ValType, + InvokeError, IrVisitor, MemArg, MemType, Memory, MemoryKind, Module, ModuleRef, ResultType, + ResumeError, ValType, Value, WasmRef, }; extern crate std; @@ -680,6 +681,15 @@ mod tests { test_op!(test_i32_gt_s_true, i32_gt_s, i32, i32: 5, (-5i32) as u32 => 1); test_op!(test_i32_le_s_true, i32_le_s, i32, i32: 5, 5 => 1); test_op!(test_i32_ge_s_true, i32_ge_s, i32, i32: 5, 5 => 1); + test_op!(test_i32_gt_s_false, i32_gt_s, i32, i32: (-5i32) as u32, 5 => 0); + test_op!(test_i32_gt_u_true, i32_gt_u, i32, i32: (-1i32) as u32, 1 => 1); + test_op!(test_i32_gt_u_false, i32_gt_u, i32, i32: 1, (-1i32) as u32 => 0); + test_op!(test_i32_le_s_false, i32_le_s, i32, i32: 6, 5 => 0); + test_op!(test_i32_le_u_true, i32_le_u, i32, i32: 1, (-1i32) as u32 => 1); + test_op!(test_i32_le_u_false, i32_le_u, i32, i32: (-1i32) as u32, 1 => 0); + test_op!(test_i32_ge_s_false, i32_ge_s, i32, i32: (-1i32) as u32, 1 => 0); + test_op!(test_i32_ge_u_true, i32_ge_u, i32, i32: (-1i32) as u32, 1 => 1); + test_op!(test_i32_ge_u_false, i32_ge_u, i32, i32: 1, (-1i32) as u32 => 0); // ===== i32 Arithmetic Operations ===== test_op!(test_i32_clz, i32_clz, i32: 0x00F00000 => 8); @@ -1154,4 +1164,62 @@ mod tests { assert_eq!(state.stack.read_u64(0), 1); // Wraps around }); } + + // ===== Engine invoke/resume failure paths ===== + + /// `invoke` on a non-idle engine must report `Busy` instead of panicking. + #[test] + fn test_invoke_busy_when_not_idle() { + with_test_context(|state| { + // Force a non-idle stack pointer to simulate a mid-execution engine. + // The busy guard runs before any module/function lookup, so the + // (empty) test module's function table is never touched. + state.sp = 1; + + let err = state.invoke( + WasmRef { + module: ModuleRef(0), + index: 0, + }, + &[], + ); + assert_eq!(err, Err(InvokeError::Busy)); + }); + } + + /// `resume` on an engine that is not paused must report `NotPaused`. + #[test] + fn test_resume_when_not_paused() { + with_test_context(|state| { + assert!(state.host_pause_result.is_none()); + assert_eq!(state.resume(None), Err(ResumeError::NotPaused)); + }); + } + + /// `resume` with a value that does not match the paused host function's + /// declared result type must report `ResultTypeMismatch` and preserve the + /// pending pause so the caller can retry. + #[test] + fn test_resume_result_type_mismatch_preserves_pause() { + with_test_context(|state| { + // Simulate a host function that paused while expecting an i32 result. + state.host_pause_result = Some(ResultType(Some(ValType::I32))); + + // Supplying no value (or a wrong-typed value) is a mismatch. + assert_eq!(state.resume(None), Err(ResumeError::ResultTypeMismatch)); + // The pending pause is left intact for a retry. + assert_eq!( + state.host_pause_result, + Some(ResultType(Some(ValType::I32))) + ); + + // Retrying with the correctly-typed value succeeds and consumes the + // pause, pushing the value onto the operand stack. + let sp_before = state.sp; + assert_eq!(state.resume(Some(Value::I32(7))), Ok(())); + assert!(state.host_pause_result.is_none()); + assert_eq!(state.sp, sp_before + 1); + assert_eq!(state.stack.read_u32(sp_before), 7); + }); + } } diff --git a/src/ir_reader.rs b/src/ir_reader.rs index f9add16..a7436ad 100644 --- a/src/ir_reader.rs +++ b/src/ir_reader.rs @@ -8,6 +8,17 @@ impl AddAssign for JumpTarget { } } +impl JumpTarget { + fn advance(&mut self, words: u32) { + debug_assert!( + (self.0 as u64) + (words as u64) <= u32::MAX as u64, + "IR address overflow advancing {words} word(s) past {:#010x}", + self.0 + ); + *self += words as i32; + } +} + pub(crate) struct IrReader; impl IrReader { fn read(code: &[Box], address: &mut JumpTarget) -> u16 { @@ -19,7 +30,7 @@ impl IrReader { if page >= code.len() || offset >= 256 { panic!("invalid read address"); } else { - *address += 1; + address.advance(1); code[page].0[offset] } } @@ -27,7 +38,7 @@ impl IrReader { #[cfg(not(feature = "strict-assertions"))] { let v = unsafe { code.get_unchecked(page).0.get_unchecked(offset) }; - *address += 1; + address.advance(1); *v } } @@ -66,6 +77,15 @@ impl IrReader { let opcode = (first >> 8) as u8; let imm = (first & 0xFF) as u8; + macro_rules! imm_valtype { + () => { + match ValType::from_repr(imm) { + Some(ty) => ty, + None => return visitor.unreachable(state), + } + }; + } + macro_rules! instruction { // Instruction with no operands ($name:ident) => {{ @@ -78,7 +98,7 @@ impl IrReader { visitor.$name( LocalVariable { frame_offset, - ty: imm.into(), + ty: imm_valtype!(), }, state, )?; @@ -184,10 +204,10 @@ impl IrReader { // Parametric instructions DROP => { - visitor.drop(imm.into(), state)?; + visitor.drop(imm_valtype!(), state)?; } SELECT => { - visitor.select(imm.into(), state)?; + visitor.select(imm_valtype!(), state)?; } // Variable instructions @@ -425,7 +445,7 @@ impl IrReader { F64_CONVERT_I64_S => instruction!(f64_convert_i64_s), F64_CONVERT_I64_U => instruction!(f64_convert_i64_u), F64_PROMOTE_F32 => instruction!(f64_promote_f32), - _ => panic!("invalid opcode {opcode:?}"), + _ => return visitor.unreachable(state), } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 1e09bdc..eebd809 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,36 +60,10 @@ pub(crate) use ir_reader::*; mod engine; pub use engine::*; -#[derive(Debug, Default, Clone)] -#[repr(C)] -pub struct MemoryStatistics { - pub total_bytes: i32, - pub pad_bytes: i32, -} - -/// Computes the delta between two different statistic samples -impl core::ops::Sub for MemoryStatistics { - type Output = MemoryStatistics; - - fn sub(self, rhs: Self) -> Self::Output { - MemoryStatistics { - total_bytes: self.total_bytes - rhs.total_bytes, - pad_bytes: self.pad_bytes - rhs.pad_bytes, - } - } -} - -impl core::ops::AddAssign for MemoryStatistics { - fn add_assign(&mut self, rhs: Self) { - self.total_bytes += rhs.total_bytes; - self.pad_bytes += rhs.pad_bytes; - } -} - #[cfg(any(test, kani))] pub mod test_support { - use crate::MemoryStatistics; use crate::alloc::{AllocError, Allocator}; + use core::sync::atomic::{AtomicI32, Ordering}; extern crate std; use std::alloc::Layout; @@ -99,7 +73,7 @@ pub mod test_support { pub struct RustSystemAllocator; // Track allocation statistics - static mut TOTAL_ALLOCATED: i32 = 0; + static TOTAL_ALLOCATED: AtomicI32 = AtomicI32::new(0); unsafe impl Allocator for RustSystemAllocator { unsafe fn alloc(&self, layout: Layout) -> Result<*mut u8, AllocError> { @@ -108,9 +82,7 @@ pub mod test_support { } else { let ptr = unsafe { std::alloc::alloc(layout) }; if !ptr.is_null() { - unsafe { - TOTAL_ALLOCATED += layout.size() as i32; - } + TOTAL_ALLOCATED.fetch_add(layout.size() as i32, Ordering::Relaxed); } Ok(ptr) } @@ -123,22 +95,14 @@ pub mod test_support { unsafe { std::alloc::dealloc(ptr, layout); - TOTAL_ALLOCATED -= layout.size() as i32; - } - } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: unsafe { TOTAL_ALLOCATED }, - pad_bytes: 0, } + TOTAL_ALLOCATED.fetch_sub(layout.size() as i32, Ordering::Relaxed); } } } #[cfg(test)] mod tests { - use crate::MemoryStatistics; use crate::alloc::Allocator; use crate::test_support::RustSystemAllocator; use core::alloc::Layout; @@ -169,8 +133,4 @@ mod tests { let layout = Layout::from_size_align(size, align).unwrap(); unsafe { (*GLOBAL_ALLOCATOR).dealloc(ptr, layout) } } - #[unsafe(no_mangle)] - pub unsafe extern "C" fn __spacewasm_memory_statistics() -> MemoryStatistics { - unsafe { (*GLOBAL_ALLOCATOR).memory_statistics() } - } } diff --git a/src/memory.rs b/src/memory.rs index 951c3c8..aec947e 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -10,7 +10,16 @@ pub trait WasmMemoryAllocator { /// Allocate a new memory region for linear memory fn allocate(&self, layout: Layout) -> Result, AllocError>; - /// Reallocate a memory region moving data if needed + /// Reallocate a memory region, moving data if needed. + /// + /// On success, returns a pointer to a block of `layout` whose leading + /// `old_layout.size()` bytes hold the previous contents; the old allocation + /// (`ptr`/`old_layout`) has been consumed and must not be used again. + /// + /// # Failure ownership contract + /// + /// On `Err`, the implementation MUST leave the original allocation + /// (`ptr` with `old_layout`) valid, unmoved, and still owned by the caller. fn reallocate( &self, ptr: NonNull, @@ -31,6 +40,13 @@ impl Rc { } } +/// A WebAssembly linear memory +/// +/// Linear memory implements inner mutability so it is not thread-safe. +/// `Memory` holds a raw pointer, so it is automatically `!Send + !Sync`. +/// +/// Needs to have inner mutability because multiple modules can point +/// to it (via imports) and we use Rc for reference counting these ownerships. pub struct Memory { ptr: *mut u8, size: usize, @@ -288,6 +304,22 @@ impl Memory { unsafe { slice::from_raw_parts(self.ptr, self.size) } } } + + /// Expose linear memory as `&[UnsafeCell]`. + /// + /// [`get_slice`]: Memory::get_slice + pub fn as_shared_cells(&self) -> &[core::cell::UnsafeCell] { + if self.ptr.is_null() { + &[] + } else { + unsafe { + core::slice::from_raw_parts( + self.ptr as *const core::cell::UnsafeCell, + self.size, + ) + } + } + } } impl Drop for Memory { @@ -481,6 +513,31 @@ mod tests { assert_eq!(Memory::effective_address(10, 20).unwrap(), 30); } + #[test] + fn as_shared_cells_aliases_and_mutates() { + let mem = Memory::new(mem_type(1, None), allocator()).unwrap(); + + // The cell view covers exactly the linear memory bytes. + let cells = mem.as_shared_cells(); + assert_eq!(cells.len(), mem.get_slice().len()); + + // A store is observable through the cell view... + mem.store_u8(3, 0x5A).unwrap(); + assert_eq!(unsafe { *cells[3].get() }, 0x5A); + + // ...and a write through a cell is observable through a load, which is + // the shared-but-mutable access wiggle relies on. + unsafe { + *cells[7].get() = 0xC3; + } + assert_eq!(mem.load_u8(7).unwrap(), 0xC3); + } + + #[test] + fn as_shared_cells_zero_memory_is_empty() { + assert!(Memory::zero().as_shared_cells().is_empty()); + } + #[test] fn memory_error_from_alloc_error() { assert_eq!( diff --git a/src/module.rs b/src/module.rs index 0ec780e..059e2b9 100644 --- a/src/module.rs +++ b/src/module.rs @@ -79,7 +79,6 @@ impl Module { code_builder, &mut DefaultCustomSectionHandler, allocator, - None, ) .map_err(|err| ParseError { offset: wasm.offset() as u32, @@ -87,33 +86,6 @@ impl Module { }) } - pub fn new_with_statistics( - name: &str, - stream: &mut dyn WasmStream, - store: &mut Store, - code_builder: &mut CodeBuilder, - allocator: Rc, - ) -> Result<(Module, [MemoryStatistics; SectionKind::N as usize]), ParseError> { - let mut wasm = Reader::new(stream); - let mut stats: [MemoryStatistics; SectionKind::N as usize] = Default::default(); - - let m = Module::read::( - name, - &mut wasm, - store, - code_builder, - &mut DefaultCustomSectionHandler, - allocator, - Some(&mut stats), - ) - .map_err(|err| ParseError { - offset: wasm.offset() as u32, - err, - })?; - - Ok((m, stats)) - } - #[allow(clippy::too_many_arguments)] fn read( name: &str, @@ -122,7 +94,6 @@ impl Module { code_builder: &mut CodeBuilder, custom_handler: &mut dyn CustomSectionHandler, allocator: Rc, - mut stats: Option<&mut [MemoryStatistics; SectionKind::N as usize]>, ) -> Result { let magic = wasm.strip_bytes::<4>()?; if magic != [0x00, 0x61, 0x73, 0x6D] { @@ -190,26 +161,19 @@ impl Module { let section_size = wasm.read_u32()? as usize; let section_start = wasm.offset(); - let memory_before = GlobalAllocator.memory_statistics(); - - module - .read_section::( - wasm, - store, - section_size, - section_ty, - custom_handler, - code_builder, - allocator.clone(), - ) - .map_err(|e| e.with_section(section_ty))?; - - let memory_after = GlobalAllocator.memory_statistics(); - - // Compute the memory usage delta to track per-section usage - if let Some(stats) = &mut stats { - stats[section_ty as usize] += memory_after - memory_before; - } + wasm.with_limit(section_size, |wasm| { + module + .read_section::( + wasm, + store, + section_size, + section_ty, + custom_handler, + code_builder, + allocator.clone(), + ) + .map_err(|e| e.with_section(section_ty)) + })?; // Validate we actually read the entire section let section_end = wasm.offset(); @@ -516,13 +480,18 @@ impl SectionKind { pub struct CustomSection; impl CustomSection { + pub const MAX_NAME_LENGTH: usize = 32; + pub fn read( wasm: &mut Reader, size: usize, handler: &mut dyn CustomSectionHandler, ) -> Result<(), ValidationError> { let start = wasm.offset(); - let name: StaticVec = wasm.read_vec_stack(|w| w.read_u8())?; + + // We only support up to MAX_NAME_LENGTH-byte names + let name: StaticVec = + wasm.read_vec_stack(|w| w.read_u8())?; let name_str = core::str::from_utf8(&name).map_err(|_| ValidationError::MalformedUtf8)?; let name_length = wasm.offset() - start; @@ -530,7 +499,10 @@ impl CustomSection { return Err(ValidationError::MalformedSectionSize); } - handler.custom_section(name_str, size - name_length, wasm) + let payload_len = size - name_length; + wasm.with_limit(payload_len, |wasm| { + handler.custom_section(name_str, payload_len, wasm) + }) } } diff --git a/src/reader.rs b/src/reader.rs index 0a97760..7cc703c 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -51,6 +51,10 @@ pub struct Reader<'wasm> { /// A counter keeping track of the total number of bytes we've processed in the Wasm binary /// This is useful for generating error messages with an absolute location in the binary. full_offset: usize, + + /// Offset ahead of full_offset tracking the read limits of this section. + /// Set by `with_limit` and restored to usize::MAX (or previous limit) after completion. + limit: usize, } impl<'wasm> Reader<'wasm> { @@ -61,6 +65,7 @@ impl<'wasm> Reader<'wasm> { next: None, buffer: CircularBuffer::new(), full_offset: 0, + limit: usize::MAX, } } @@ -68,6 +73,30 @@ impl<'wasm> Reader<'wasm> { self.full_offset } + /// Run `f` with reads bounded to `len` bytes. + pub fn with_limit(&mut self, len: usize, f: F) -> Result + where + E: From, + F: FnOnce(&mut Self) -> Result, + { + let new_limit = self + .full_offset + .checked_add(len) + .filter(|&nl| nl <= self.limit) + .ok_or(ValidationError::MalformedSectionSize)?; + let prev = core::mem::replace(&mut self.limit, new_limit); + let result = f(self); + self.limit = prev; + result + } + + /// Bytes that may still be read before reaching the configured + /// section/function limit (see [`with_limit`](Self::with_limit)). Returns + /// `usize::MAX` when no limit is in effect. + fn bytes_remaining(&self) -> usize { + self.limit.saturating_sub(self.full_offset) + } + /// Fills the circular buffer from the stream chunks. /// This method tries to fill the buffer as much as possible from the current chunk, /// and fetches a new chunk from the stream if the current one is exhausted. @@ -143,6 +172,10 @@ impl<'wasm> Reader<'wasm> { /// Tries to read one byte and fails if the end of file is reached. pub fn read_u8(&mut self) -> Result { + if self.full_offset >= self.limit { + return Err(ValidationError::Eof); + } + let byte = self.peek_u8()?; self.buffer.pop_front(); self.full_offset += 1; @@ -442,7 +475,7 @@ impl<'wasm> Reader<'wasm> { T: 'wasm, { let len = self.read_u32()?; - if len as usize > SIZE { + if len as usize > SIZE || len as usize > self.bytes_remaining() { return Err(ValidationError::VecTooLong); } @@ -465,6 +498,10 @@ impl<'wasm> Reader<'wasm> { VA: Allocator, { let len = self.read_u32()?; + if len as usize > self.bytes_remaining() { + return Err(ValidationError::VecTooLong); + } + let mut out = Vec::new_in(alloc, len)?; for _ in 0..len { out.push(read_element(self)?); @@ -652,6 +689,156 @@ mod tests { assert_eq!(reader.offset(), data.len()); } + #[test] + fn read_vec_rejects_length_exceeding_limit() { + // A LEB128 length of 100 followed by only two bytes. With a section + // limit covering just the three available bytes, the declared length + // cannot fit (each element needs >=1 byte), so the reader rejects it + // with `VecTooLong` before attempting any allocation. + let data = [0x64u8, 0x00, 0x00]; // len = 100, then two payload bytes + let mut stream = ChunkStream::new(&data, data.len()); + let mut reader = Reader::new(&mut stream); + assert_eq!( + reader.with_limit(data.len(), |r| r.read_vec(|r| r.read_u8())), + Err(ValidationError::VecTooLong) + ); + } + + #[test] + fn read_vec_allows_length_within_limit() { + // The same length check passes when the limit accommodates the + // elements, and decoding proceeds normally. + let data = [0x03u8, 0x0A, 0x0B, 0x0C]; + let mut stream = ChunkStream::new(&data, data.len()); + let mut reader = Reader::new(&mut stream); + let v = reader + .with_limit(data.len(), |r| r.read_vec(|r| r.read_u8())) + .unwrap(); + assert_eq!(&*v, &[0x0A, 0x0B, 0x0C]); + assert_eq!(reader.offset(), data.len()); + } + + #[test] + fn read_vec_unbounded_without_limit() { + // Without `with_limit`, `bytes_remaining()` is `usize::MAX`, so the + // bound is inert and behaviour matches the pre-existing reader. + let data = [0x03u8, 0x0A, 0x0B, 0x0C]; + let mut stream = ChunkStream::new(&data, data.len()); + let mut reader = Reader::new(&mut stream); + let v = reader.read_vec(|r| r.read_u8()).unwrap(); + assert_eq!(&*v, &[0x0A, 0x0B, 0x0C]); + } + + /// Assert that reading a value needing `need` bytes succeeds under a + /// `with_limit(need, …)` region that ends exactly at the value's last byte, + /// and is rejected with `Eof` under a `with_limit(need - 1, …)` region that + /// is one byte too small (the read crosses the boundary). Bytes are fed one + /// per chunk so the boundary can fall inside a multi-byte decode. + fn bounded_read( + data: &[u8], + need: usize, + mut f: impl FnMut(&mut Reader) -> Result, + ) { + // Exact fit: the region ends exactly at the value's final byte. + let mut stream = ChunkStream::new(data, 1); + let mut reader = Reader::new(&mut stream); + reader + .with_limit(need, |r| f(r)) + .expect("exact-fit read should succeed"); + assert_eq!(reader.offset(), need, "offset after exact-fit read"); + drop(reader); + + // One byte short: the read crosses the boundary and is rejected as Eof. + let mut stream = ChunkStream::new(data, 1); + let mut reader = Reader::new(&mut stream); + assert_eq!( + reader.with_limit(need - 1, |r| f(r)), + Err(ValidationError::Eof), + "read crossing the boundary should be Eof", + ); + } + + #[test] + fn with_limit_bounds_every_primitive_read() { + bounded_read(&[0xAA, 0xBB], 1, |r| r.read_u8()); + // 3-byte unsigned LEB128 (624485). + bounded_read(&[0xE5, 0x8E, 0x26], 3, |r| r.read_u32()); + // 10-byte signed LEB128 (i64::MIN), the longest continuation path. + let min = [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7F]; + bounded_read(&min, 10, |r| r.read_i64()); + bounded_read(&3.5f32.to_le_bytes(), 4, |r| r.read_f32_bits()); + bounded_read(&[0x11, 0x22, 0x33, 0x44], 4, |r| r.strip_bytes::<4>()); + bounded_read(&[0x01, 0x02, 0x03, 0x04], 4, |r| r.skip(4)); + // `expect_u8` peeks (unbounded) then consumes via `read_u8`; a matching + // byte at a zero-remaining boundary is still rejected on the consume. + bounded_read(&[0x07], 1, |r| r.expect_u8(0x07)); + } + + #[test] + fn with_limit_nests_and_rejects_oversized_subregion() { + let data = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06]; + + // An inner region is bounded within the outer region, and the bytes the + // inner region does not consume remain readable in the outer region. + let mut stream = ChunkStream::new(&data, 1); + let mut reader = Reader::new(&mut stream); + let r: Result<(), ValidationError> = reader.with_limit(4, |r| { + r.with_limit(2, |r| { + assert_eq!(r.read_u8()?, 0x01); + assert_eq!(r.read_u8()?, 0x02); + // A third read crosses the inner 2-byte boundary. + assert_eq!(r.read_u8(), Err(ValidationError::Eof)); + Ok::<(), ValidationError>(()) + })?; + // Still within the outer 4-byte budget: the next byte reads fine. + assert_eq!(r.read_u8()?, 0x03); + Ok(()) + }); + assert_eq!(r, Ok(())); + drop(reader); + + // An inner region claiming more bytes than the enclosing budget is + // rejected before any read. + let mut stream = ChunkStream::new(&data, 1); + let mut reader = Reader::new(&mut stream); + let r = reader.with_limit(3, |r| r.with_limit(4, |r| r.read_u8())); + assert_eq!(r, Err(ValidationError::MalformedSectionSize)); + } + + #[test] + fn with_limit_restores_outer_bound_after_ok_and_err() { + let data = [0x0Au8, 0x0B, 0x0C, 0x0D]; + + // After a bounded region completes with Ok, the limit is restored: a + // subsequent framing read proceeds past the just-closed boundary. + let mut stream = ChunkStream::new(&data, 1); + let mut reader = Reader::new(&mut stream); + reader + .with_limit(2, |r| { + assert_eq!(r.read_u8()?, 0x0A); + assert_eq!(r.read_u8()?, 0x0B); + Ok::<(), ValidationError>(()) + }) + .unwrap(); + assert_eq!(reader.read_u8().unwrap(), 0x0C); + assert_eq!(reader.read_u8().unwrap(), 0x0D); + assert_eq!(reader.offset(), data.len()); + drop(reader); + + // The limit is also restored when the region returns Err: the byte that + // was blocked at the boundary is readable once the region unwinds. + let mut stream = ChunkStream::new(&data, 1); + let mut reader = Reader::new(&mut stream); + let r: Result<(), ValidationError> = reader.with_limit(1, |r| { + assert_eq!(r.read_u8()?, 0x0A); + r.read_u8()?; // crosses the 1-byte boundary -> Eof + Ok(()) + }); + assert_eq!(r, Err(ValidationError::Eof)); + assert_eq!(reader.read_u8().unwrap(), 0x0B); + assert_eq!(reader.offset(), 2); + } + #[test] fn single_large_chunk_refills_circular_buffer_repeatedly() { // A payload larger than the 64-byte circular buffer, delivered as a diff --git a/src/store.rs b/src/store.rs index 7b7b94e..52dcdfa 100644 --- a/src/store.rs +++ b/src/store.rs @@ -59,14 +59,19 @@ impl Store { self.modules.pop() } - /// Push a module onto the store - /// Panics if the store is at capacity + /// Push a module onto the store, returning the [`ModuleRef`] of the newly + /// appended module. + /// + /// Returns [`AllocError::OutOfMemory`] when the store is already at capacity + /// (the module count configured via [`Store::from_host_modules`] / + /// [`Engine::new`]) instead of panicking. #[inline(always)] - pub fn push_module(&mut self, module: Module) { - self.modules.push(module); + pub fn push_module(&mut self, module: Module) -> Result { + self.modules.try_push(module)?; + Ok(ModuleRef((self.modules.len() - 1) as u8)) } - pub fn get_memory(&mut self, module_ref: ModuleRef) -> &Rc { + pub fn get_memory(&self, module_ref: ModuleRef) -> &Rc { match &self.modules[module_ref.0 as usize].memory { None => &self.zero_memory, Some(MemoryKind::Owned(mem)) => mem, @@ -85,7 +90,7 @@ impl Store { } } - pub fn get_table(&mut self, module_ref: ModuleRef) -> &Rc<[TableElement]> { + pub fn get_table(&self, module_ref: ModuleRef) -> &Rc<[TableElement]> { match &self.modules[module_ref.0 as usize].table { None => &self.zero_table, Some(TableKind::Owned(table)) => &table.0, @@ -187,8 +192,7 @@ impl Engine { /// Note: The start function still needs to be run (if there is one) /// Returns the ModuleRef of the new module pub fn push_module(&mut self, module: Module) -> Result { - self.store.modules.try_push(module)?; - Ok(ModuleRef((self.store.modules.len() - 1) as u8)) + self.store.push_module(module) } /// Returns `true` if the engine is idle (not currently executing) @@ -236,7 +240,9 @@ impl Engine { index: u16, args: &[Value], ) -> HostFunctionResult { - let f = self.store.host_modules[module.0 as usize].functions[index as usize].get_call(); + let f = self.store.host_modules[module.0 as usize].functions[index as usize] + .get_call() + .unwrap(); // fails if someone took this function without finishing the call. let r = f(self, args); self.store.host_modules[module.0 as usize].functions[index as usize].finish_call(f); r diff --git a/src/stream.rs b/src/stream.rs index fb40e6f..bcd59e0 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -2,6 +2,7 @@ use core::ops::Deref; use crate::util::InnerVec; +/// A borrowed view over a buffer owned by a [`WasmStream`]. pub struct Chunk(InnerVec); impl From> for Chunk { @@ -19,7 +20,10 @@ impl Chunk { impl Drop for Chunk { fn drop(&mut self) { - assert_eq!(self.0.capacity, 0); + debug_assert_eq!( + self.0.capacity, 0, + "Chunk dropped without returning its buffer to the stream" + ); } } diff --git a/src/text.rs b/src/text.rs index 43a58c8..e7a2153 100644 --- a/src/text.rs +++ b/src/text.rs @@ -230,6 +230,10 @@ pub struct GlobalVariable { pub mutable: bool, } +/// The IR encodes code addresses as 24-bit page indices (see [`JumpTarget`]), +/// so no more than `2^24` code pages can ever be addressed. +const MAX_ADDRESSABLE_CODE_PAGES: u32 = 1 << 24; + #[derive(Debug, Copy, Clone)] pub struct CompilerOptions { /// Allow compiling memory.grow instructions into IR @@ -238,10 +242,24 @@ pub struct CompilerOptions { /// Maximum number of iterations to resolve during a control flow backpatch. /// This effectively limits a potentially long loop though can reject valid programs. /// - /// Set this to 0 for unlimited iterations - pub max_backpatch_iterations: u32, + /// `None` means unlimited iterations; `Some(n)` rejects a backpatch chain + /// longer than `n` with [`ValidationError::PossibleBackpatchCycle`]. This is + /// an explicit opt-out rather than a magic sentinel value. + pub max_backpatch_iterations: Option, - /// The maximum number of code pages allowed across all modules + /// The maximum number of code pages allowed across all modules. + /// + /// # Sizing + /// Each page is a [`TextPage`] of 256 16-bit words (512 bytes). At + /// construction ([`CodeBuilder::new`]) this set the *page table* capacity: + /// a `Vec` of `max_code_pages` `Box` pointers. + /// + /// The 512-byte page bodies themselves are allocated lazily as the module is loaded. + /// + /// Choose `max_code_pages` to cover the largest total IR (across all modules + /// loaded onto the store). Oversizing it costs only the pointer table, not the page bodies. + /// It must be strictly less than `MAX_ADDRESSABLE_CODE_PAGES` (`2^24`) and must be sized to + /// fit inside a single heap memory page. pub max_code_pages: u32, } @@ -256,7 +274,7 @@ pub struct CodeBuilder { impl CodeBuilder { pub fn new(options: CompilerOptions) -> Result { - if options.max_code_pages >= (1 << 24) { + if options.max_code_pages >= MAX_ADDRESSABLE_CODE_PAGES { return Err(AllocError::AllocationFailed); } @@ -298,10 +316,10 @@ impl CodeBuilder { next = address + lt.jump(); n += 1; - if self.options().max_backpatch_iterations != 0 - && n > self.options().max_backpatch_iterations - { - return Err(ValidationError::PossibleBackpatchCycle); + if let Some(max) = self.options().max_backpatch_iterations { + if n > max { + return Err(ValidationError::PossibleBackpatchCycle); + } } } @@ -338,7 +356,7 @@ impl CodeBuilder { }; // We support up to 24-bit page addresses - assert!(page_index < (1 << 24)); + assert!(page_index < MAX_ADDRESSABLE_CODE_PAGES as usize); assert!(offset < 256); JumpTarget(((page_index as u32) << 8) | (offset as u32)) @@ -515,7 +533,7 @@ impl<'a, const MAX_CONTROL_FRAMES: usize, const MAX_STACK_DEPTH: usize> pub fn check_br_table_result(&mut self, r: ResultType) -> Result<(), ValidationError> { if let Some(other) = self.br_table_result { if other != r { - Err(ValidationError::BlockResultTypeMismatch) + Err(ValidationError::BrTableResultTypeMismatch) } else { Ok(()) } @@ -531,7 +549,7 @@ impl<'a, const MAX_CONTROL_FRAMES: usize, const MAX_STACK_DEPTH: usize> ) -> Result<(), ValidationError> { if let Some(other) = self.br_table_result { if other != def_result { - Err(ValidationError::BlockResultTypeMismatch) + Err(ValidationError::BrTableResultTypeMismatch) } else { self.br_table_result = None; Ok(()) diff --git a/src/types.rs b/src/types.rs index ee9af0e..0810dc2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -26,24 +26,6 @@ pub enum ValType { F64, } -impl From for ValType { - fn from(val: u8) -> Self { - #[cfg(feature = "strict-assertions")] - match val { - 0 => ValType::I32, - 1 => ValType::I64, - 2 => ValType::F32, - 3 => ValType::F64, - _ => unreachable!(), - } - - #[cfg(not(feature = "strict-assertions"))] - unsafe { - core::mem::transmute(val) - } - } -} - /// A runtime type-tracked value #[derive(Clone, Copy, PartialEq, Debug)] pub enum Value { @@ -63,6 +45,18 @@ impl ValType { } } + /// Convert the internal `#[repr(u8)]` discriminant back into a [`ValType`], + /// returning `None` for any tag that is not a valid ValType. + pub(crate) fn from_repr(tag: u8) -> Option { + match tag { + 0 => Some(ValType::I32), + 1 => Some(ValType::I64), + 2 => Some(ValType::F32), + 3 => Some(ValType::F64), + _ => None, + } + } + fn convert(v: u8) -> Result { // Value types are encoded by a single byte. use ValType::*; @@ -467,6 +461,17 @@ impl GlobalType { mod tests { use super::*; + #[test] + fn valtype_from_repr_roundtrips_and_rejects() { + // Every valid discriminant round-trips through `as u8` / `from_repr`. + for vt in [ValType::I32, ValType::I64, ValType::F32, ValType::F64] { + assert_eq!(ValType::from_repr(vt as u8), Some(vt)); + } + // Any out-of-range tag is rejected instead of being transmuted. + assert_eq!(ValType::from_repr(4), None); + assert_eq!(ValType::from_repr(0xFF), None); + } + #[test] fn raw_value_roundtrip_word() { // The 32-bit accessors only touch the low word; the high bits stay clear. diff --git a/src/util/alloc.rs b/src/util/alloc.rs index 91fb3df..55dc251 100644 --- a/src/util/alloc.rs +++ b/src/util/alloc.rs @@ -2,7 +2,7 @@ // (https://github.com/rust-lang/rust), licensed under Apache-2.0. These // portions have been modified for SpaceWasm. -use core::{alloc::Layout, marker::PhantomData}; +use core::alloc::Layout; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AllocError { @@ -32,8 +32,6 @@ impl From for u32 { } } -use crate::MemoryStatistics; - unsafe extern "C" { /// Allocate a pointer on the heap (or wherever) given a size and alignment. /// If allocation could not succeed, write the error code corresponding @@ -42,14 +40,30 @@ unsafe extern "C" { /// Deallocate a pointer given it's size and alignment fn __spacewasm_dealloc(ptr: *mut u8, size: usize, align: usize); - - /// Get basic information about the allocation statistics - fn __spacewasm_memory_statistics() -> MemoryStatistics; } +/// Installs the process-global allocator that backs the `__spacewasm_alloc` / +/// `__spacewasm_dealloc` / `__spacewasm_memory_statistics` FFI symbols. +/// +/// # Single-threaded requirement (UB otherwise) +/// +/// The generated code stores the allocator instance and a pointer to it in +/// `static mut` globals and dereferences them from the exported `extern "C"` +/// functions **without any synchronization**. This is only sound in a +/// single-threaded environment, which is the model SpaceWasm targets: the +/// interpreter and its allocator run on a single thread. +/// +/// Calling any of the generated `__spacewasm_*` functions concurrently from +/// more than one thread (or re-entrantly, e.g. allocating from within an +/// allocator callback that is itself mid-allocation) creates aliasing +/// `&mut`/`&` references to the `static mut` state and is **undefined +/// behavior**. Embedders that need multi-threaded access must provide their own +/// synchronization *around* these entry points. #[macro_export] macro_rules! global_allocator { ($ty: ty, $val:expr) => { + // SAFETY: see the macro's doc comment -- these `static mut` globals are + // sound only under the single-threaded, non-re-entrant usage contract. static mut ALLOC_IMPL: $ty = $val; #[allow(unused_unsafe)] @@ -88,11 +102,6 @@ macro_rules! global_allocator { }; unsafe { $crate::Allocator::dealloc(&*GLOBAL_ALLOCATOR, ptr, layout) } } - - #[unsafe(no_mangle)] - pub unsafe extern "C" fn __spacewasm_memory_statistics() -> $crate::MemoryStatistics { - unsafe { $crate::Allocator::memory_statistics(&*GLOBAL_ALLOCATOR) } - } }; } @@ -114,7 +123,6 @@ pub unsafe trait Allocator { /// # Safety /// The caller must ensure that `ptr` was allocated by this allocator with the given `layout`. unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout); - fn memory_statistics(&self) -> MemoryStatistics; } unsafe impl Allocator for &T { @@ -125,49 +133,6 @@ unsafe impl Allocator for &T { unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { (**self).dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> MemoryStatistics { - (**self).memory_statistics() - } -} - -pub struct StaticAllocator<'a, const N: usize> { - data: *mut u8, - _phantom: PhantomData<&'a [u8; N]>, -} - -impl<'a, const N: usize> StaticAllocator<'a, N> { - pub fn new(data: &'a mut [u8; N]) -> Self { - StaticAllocator { - data: data.as_mut_ptr(), - _phantom: PhantomData, - } - } - - pub fn new_from_ptr(data: *mut u8) -> Self { - StaticAllocator { - data, - _phantom: PhantomData, - } - } -} - -unsafe impl<'a, const N: usize> Allocator for StaticAllocator<'a, N> { - unsafe fn alloc(&self, layout: Layout) -> Result<*mut u8, AllocError> { - assert_eq!(layout.size(), N); - Ok(self.data) - } - - unsafe fn dealloc(&self, ptr: *mut u8, _: Layout) { - assert_eq!(ptr, self.data) - } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: 0, - pad_bytes: 0, - } - } } #[derive(Clone, Copy)] @@ -187,10 +152,6 @@ unsafe impl Allocator for GlobalAllocator { unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { __spacewasm_dealloc(ptr, layout.size(), layout.align()) } } - - fn memory_statistics(&self) -> MemoryStatistics { - unsafe { __spacewasm_memory_statistics() } - } } #[cfg(test)] @@ -228,78 +189,16 @@ mod tests { #[test] fn test_reference_allocator_delegates() { // The blanket `impl Allocator for &T` should forward every call to the - // underlying allocator. - let mut backing = [0u8; 4]; - let expected = backing.as_mut_ptr(); - let alloc = StaticAllocator::<4>::new(&mut backing); + // underlying allocator. This is the form `global_allocator!` relies on, + // allocating through `&*GLOBAL_ALLOCATOR`. + let alloc = GlobalAllocator; let by_ref = &alloc; - let layout = Layout::from_size_align(4, 1).unwrap(); + let layout = Layout::from_size_align(32, 8).unwrap(); let ptr = unsafe { Allocator::alloc(&by_ref, layout) }.unwrap(); - assert_eq!(ptr, expected); + assert!(!ptr.is_null()); unsafe { Allocator::dealloc(&by_ref, ptr, layout) }; - - // Call through the reference impl explicitly so the `&T` forwarding - // (rather than the concrete impl via auto-deref) is exercised. - let stats = Allocator::memory_statistics(&by_ref); - assert_eq!(stats.total_bytes, 0); - assert_eq!(stats.pad_bytes, 0); - } - - #[test] - fn test_static_allocator_new() { - let mut backing = [0u8; 8]; - let expected = backing.as_mut_ptr(); - let alloc = StaticAllocator::<8>::new(&mut backing); - - let layout = Layout::from_size_align(8, 1).unwrap(); - let ptr = unsafe { alloc.alloc(layout) }.unwrap(); - assert_eq!(ptr, expected); - - unsafe { alloc.dealloc(ptr, layout) }; - } - - #[test] - fn test_static_allocator_new_from_ptr() { - let mut backing = [0u8; 16]; - let raw = backing.as_mut_ptr(); - let alloc = StaticAllocator::<16>::new_from_ptr(raw); - - let layout = Layout::from_size_align(16, 1).unwrap(); - let ptr = unsafe { alloc.alloc(layout) }.unwrap(); - assert_eq!(ptr, raw); - - unsafe { alloc.dealloc(ptr, layout) }; - } - - #[test] - fn test_static_allocator_memory_statistics() { - let mut backing = [0u8; 4]; - let alloc = StaticAllocator::<4>::new(&mut backing); - let stats = alloc.memory_statistics(); - assert_eq!(stats.total_bytes, 0); - assert_eq!(stats.pad_bytes, 0); - } - - #[test] - #[should_panic] - fn test_static_allocator_alloc_wrong_size_panics() { - let mut backing = [0u8; 4]; - let alloc = StaticAllocator::<4>::new(&mut backing); - // The layout size must equal the const generic N. - let layout = Layout::from_size_align(2, 1).unwrap(); - let _ = unsafe { alloc.alloc(layout) }; - } - - #[test] - #[should_panic] - fn test_static_allocator_dealloc_wrong_ptr_panics() { - let mut backing = [0u8; 4]; - let alloc = StaticAllocator::<4>::new(&mut backing); - let layout = Layout::from_size_align(4, 1).unwrap(); - let mut other = 0u8; - unsafe { alloc.dealloc(&mut other as *mut u8, layout) }; } #[test] @@ -320,40 +219,4 @@ mod tests { let result = unsafe { alloc.alloc(layout) }; assert_eq!(result, Err(AllocError::AllocationFailed)); } - - #[test] - fn test_global_allocator_memory_statistics() { - // Just ensure the FFI passthrough is callable and returns a value. - let _ = GlobalAllocator.memory_statistics(); - } - - #[test] - fn test_memory_statistics_sub() { - let s1 = MemoryStatistics { - total_bytes: 100, - pad_bytes: 20, - }; - let s2 = MemoryStatistics { - total_bytes: 60, - pad_bytes: 10, - }; - let diff = s1 - s2; - assert_eq!(diff.total_bytes, 40); - assert_eq!(diff.pad_bytes, 10); - } - - #[test] - fn test_memory_statistics_add_assign() { - let mut s1 = MemoryStatistics { - total_bytes: 100, - pad_bytes: 20, - }; - let s2 = MemoryStatistics { - total_bytes: 50, - pad_bytes: 5, - }; - s1 += s2; - assert_eq!(s1.total_bytes, 150); - assert_eq!(s1.pad_bytes, 25); - } } diff --git a/src/util/box_.rs b/src/util/box_.rs index 6271386..b4ccce4 100644 --- a/src/util/box_.rs +++ b/src/util/box_.rs @@ -21,9 +21,26 @@ impl core::fmt::Debug for Box { } } +impl Box { + /// Attempt to clone the box, returning [`AllocError`] if allocating the new + /// backing storage fails. + /// + /// The inner value is cloned before the new allocation is populated, so a + /// panic in `T::clone` (or an allocation failure) leaks nothing. + pub fn try_clone(&self) -> Result { + Box::new_in(self.alloc.clone(), (**self).clone()) + } +} + impl Clone for Box { + /// # Panics + /// Panics if allocating the new backing storage fails. Use + /// [`Box::try_clone`] to handle allocation failure without panicking. + /// + /// (Mirrors the fallible-allocation decision documented on + /// [`crate::Vec::clone`].) fn clone(&self) -> Self { - Box::new_in(self.alloc.clone(), (**self).clone()).unwrap() + self.try_clone().expect("Box::clone: allocation failed") } } diff --git a/src/util/inner_vec.rs b/src/util/inner_vec.rs index dc854e9..1d3ccb6 100644 --- a/src/util/inner_vec.rs +++ b/src/util/inner_vec.rs @@ -2,10 +2,20 @@ use core::ops::{Deref, DerefMut}; use crate::AllocError; +/// The raw backing store shared by [`crate::Vec`] and the streaming API. +/// +/// # Invariants +/// - `len <= capacity`. +/// - If `capacity == 0`, `ptr` may be null; otherwise `ptr` points to a single +/// allocation valid for `capacity` values of `T`, and the first `len` +/// elements are initialized. +/// - `InnerVec` does **not** own its allocation: it never frees the buffer and +/// never drops its elements. The owner (typically [`crate::Vec`]) is +/// responsible for both. pub struct InnerVec { - pub ptr: *mut T, - pub capacity: u32, - pub len: u32, + pub(crate) ptr: *mut T, + pub(crate) capacity: u32, + pub(crate) len: u32, } impl core::fmt::Debug for InnerVec { @@ -15,6 +25,11 @@ impl core::fmt::Debug for InnerVec { } impl InnerVec { + /// Create an empty `InnerVec` with no backing allocation. + /// + /// This is the only *safe* constructor: it upholds every invariant + /// trivially with `len == capacity == 0` and a null `ptr`. Populated + /// instances must be built with [`InnerVec::from_raw_parts`]. pub fn zero() -> InnerVec { InnerVec { ptr: core::ptr::null_mut(), @@ -23,6 +38,26 @@ impl InnerVec { } } + /// Build an `InnerVec` directly from its raw parts. + /// + /// # Safety + /// The caller must uphold all of the [`InnerVec`] invariants: + /// - `len <= capacity`. + /// - If `capacity == 0`, `ptr` may be null; otherwise `ptr` must point to a + /// single allocation valid for `capacity` values of `T` (correctly sized + /// and aligned), with the first `len` elements initialized. + /// - `InnerVec` neither frees the allocation nor drops its elements, so the + /// caller (typically [`crate::Vec`]) retains ownership and must keep the + /// allocation valid for as long as the returned value is used. + pub unsafe fn from_raw_parts(ptr: *mut T, capacity: u32, len: u32) -> InnerVec { + InnerVec { ptr, capacity, len } + } + + /// Raw pointer to the backing allocation (null when `capacity == 0`). + pub fn ptr(&self) -> *mut T { + self.ptr + } + pub fn len(&self) -> usize { self.len as usize } @@ -133,11 +168,9 @@ mod kani_proofs { let layout = Layout::array::(capacity as usize).unwrap(); let ptr = unsafe { alloc.alloc(layout).unwrap() as *mut T }; - InnerVec { - ptr, - capacity, - len: 0, - } + // SAFETY: `ptr` is a fresh allocation valid for `capacity` elements and + // `len == 0`, so no element is claimed to be initialized. + unsafe { InnerVec::from_raw_parts(ptr, capacity, 0) } } // Helper to deallocate an InnerVec @@ -340,8 +373,17 @@ mod kani_proofs { } } - /// Each value dropped exactly once, iterator properly invalidates vec - /// drop_count ≤ (original_len - current_len) + /// Drop semantics of `InnerVec`: + /// - Elements held in the vec are NOT dropped by `iter()` (it yields `&T`). + /// - `pop()` moves an element out; dropping that returned value runs its + /// destructor exactly once. + /// - `InnerVec` has no `Drop` of its own, so freeing the (emptied) buffer + /// runs no destructors. + /// + /// This asserts the drop *count* against a live counter, rather than the + /// previous version whose bound was vacuously true (iterating by reference + /// never changes `len`, so `drop_count <= original_len - current_len` + /// reduced to `0 <= 0`). #[kani::proof] fn verify_drop_semantics() { unsafe { @@ -360,42 +402,42 @@ mod kani_proofs { drop_counter: &mut drop_count as *mut u32, }); - let original_len = vec.len; - assert_eq!(original_len, 2, "Should have 2 elements"); + assert_eq!(vec.len, 2, "Should have 2 elements"); + + // Iterating by reference must NOT drop any element. + for _val in vec.iter() {} + assert_eq!( + drop_count, 0, + "iter() yields references and must not drop elements" + ); - // First iteration should consume values and drop them + // Popping moves the value out; dropping it here runs one destructor. { - for _val in vec.iter() { - // Values are consumed and dropped - } + let popped = vec.pop(); + assert!(popped.is_some(), "pop on a non-empty vec returns Some"); } - - let current_len = vec.len; - let drops_after_first = drop_count; - - // drop_count ≤ (original_len - current_len) - // With correct ownership tracking, drops should equal removed elements - assert!( - drop_count <= (original_len - current_len), - "Drops must not exceed removed elements" + assert_eq!( + drop_count, 1, + "dropping one popped value runs exactly one destructor" ); + assert_eq!(vec.len, 1, "pop decrements len"); - // Second iteration over same data + // Pop and drop the second value. { - for _val in vec.iter() { - // Values already consumed - iterator should not read them again - } + let _popped = vec.pop(); } - - let drops_after_second = drop_count; - - // Each value dropped exactly once (no double-drop) assert_eq!( - drops_after_second, drops_after_first, - "No additional drops should occur on second iteration" + drop_count, 2, + "dropping the second popped value runs exactly one more destructor" ); + assert_eq!(vec.len, 0, "vec is now empty"); + // `InnerVec` owns no `Drop`, so freeing the empty buffer drops nothing. dealloc_inner_vec(vec, &alloc); + assert_eq!( + drop_count, 2, + "freeing the (empty) buffer runs no further destructors" + ); } } } diff --git a/src/util/paging.rs b/src/util/paging.rs index cf12921..5db9670 100644 --- a/src/util/paging.rs +++ b/src/util/paging.rs @@ -1,9 +1,10 @@ -use crate::MemoryStatistics; use crate::alloc::{AllocError, Allocator}; use core::alloc::Layout; use core::cell::UnsafeCell; -// TODO(tumbar) Do we need to expose this or is it constant across all of SpaceWasm? +/// Alignment requested when pulling a whole page from the backing allocator. +/// 8 is usually the largest Rust will request (for the types we are using) so this +/// default page alignment should incur minimal (probably none) initial padding. const ALIGNMENT: usize = 8; #[derive(Debug, Default, Clone)] @@ -13,15 +14,6 @@ pub struct PageAllocatorStatistics { pub pages: u32, } -impl From for MemoryStatistics { - fn from(stats: PageAllocatorStatistics) -> MemoryStatistics { - MemoryStatistics { - total_bytes: stats.total_bytes as i32, - pad_bytes: stats.pad_bytes as i32, - } - } -} - /// A page is an allocator that utilizes a large contiguous blocks of memory /// to perform smaller allocations. It is strictly increasing in it's offset for /// simplicity. This means a bounded number of allocations should occur since it does @@ -91,10 +83,6 @@ unsafe impl Allocator for PageAllocator MemoryStatistics { - self.stats().into() - } } impl PageAllocatorInner { @@ -129,8 +117,8 @@ impl PageAllocatorInner { let ptr = match page.alloc(layout) { None => { // Allocation failed on a new page - // Drop the page and error - // FIXME(tumbar) This means that the page size is too small! What do we do? + // All pages are the same size so this will never work. + // Drop the page and error. unsafe { self.page_allocator.dealloc(addr, page_layout) } return Err(AllocError::PageTooSmall); } @@ -173,6 +161,14 @@ impl PageAllocatorInner { } } } + + // This pointer is not owned by us! + // Potentially a double free or invalid pointer + debug_assert!( + false, + "PageAllocator::dealloc called with a pointer not owned by any live page \ + (foreign pointer or double free)" + ); } } @@ -351,6 +347,51 @@ mod tests { } } + #[test] + fn test_partial_deallocation_keeps_page_until_last_free() { + // A 128-byte page holds exactly two 64-byte allocations, so this + // exercises partial deallocation of a page across a multi-page live + // set: freeing one allocation from a page while its sibling is still + // live (`Page::dealloc` returning `Some(false)`) must NOT reclaim the + // page, whereas freeing the last live allocation (`Some(true)`) must. + let page_alloc = PageAllocator::::new(RustSystemAllocator, 128); + + unsafe { + let layout = Layout::from_size_align(64, 8).unwrap(); + + let a = page_alloc.alloc(layout).unwrap(); // page 0, slot 0 + let b = page_alloc.alloc(layout).unwrap(); // page 0, slot 1 (page 0 full) + let c = page_alloc.alloc(layout).unwrap(); // page 1, slot 0 + + assert_eq!(page_alloc.stats().pages, 2, "two pages must be live"); + + // Partial free: releasing one of page 0's two allocations must not + // reclaim the page while its sibling remains live. + page_alloc.dealloc(b, layout); + assert_eq!( + page_alloc.stats().pages, + 2, + "page 0 must stay live while one of its allocations remains" + ); + + // Freeing the final live allocation in page 0 reclaims it. + page_alloc.dealloc(a, layout); + assert_eq!( + page_alloc.stats().pages, + 1, + "page 0 must be reclaimed once all of its allocations are freed" + ); + + // Page 1 is still live via `c`; freeing it reclaims the last page. + page_alloc.dealloc(c, layout); + assert_eq!( + page_alloc.stats().pages, + 0, + "all pages reclaimed after every allocation is freed" + ); + } + } + #[test] fn test_page_too_small() { let page_alloc = PageAllocator::::new(RustSystemAllocator, 64); diff --git a/src/util/rc.rs b/src/util/rc.rs index 34a66b6..1e418fd 100644 --- a/src/util/rc.rs +++ b/src/util/rc.rs @@ -10,6 +10,8 @@ use core::ops::Deref; use core::panic::{RefUnwindSafe, UnwindSafe}; use core::ptr::NonNull; +/// The inner heap allocation shared by every clone of an [`Rc`]. +#[repr(C)] struct RcInner { count: Cell, value: T, @@ -90,20 +92,6 @@ impl Rc<[T], A> { len: usize, ) -> Result<(*mut T, Rc<[T], A>), AllocError> { unsafe { - if len == 0 { - // For empty slices, just allocate the RcInner with empty slice - let layout = core::alloc::Layout::new::>(); - let ptr = alloc.alloc(layout)?; - core::ptr::write(ptr as *mut Cell, Cell::new(1)); - - let rc_inner_ptr = - core::ptr::slice_from_raw_parts_mut(ptr as *mut (), 0) as *mut RcInner<[T]>; - - let rc = Self::from_inner_in(NonNull::new_unchecked(rc_inner_ptr), alloc); - return Ok((core::ptr::null_mut(), rc)); - } - - // Calculate the layout we need: Cell + align padding + [T; len] let count_layout = core::alloc::Layout::new::>(); let slice_layout = core::alloc::Layout::array::(len).map_err(|_| AllocError::OutOfMemory)?; @@ -212,6 +200,41 @@ impl Rc { fat: core::mem::ManuallyDrop, } + // SOUNDNESS / ABI ASSUMPTIONS + // + // The union below reinterprets the *representation* of a raw pointer. + // Rust does not give a *stable* guarantee for pointer layout, but every + // current Rust target follows the de-facto ABI relied on here: + // + // 1. A pointer to an unsized type (`*const U` where `U` is a trait + // object or slice) is a two-word "fat" pointer laid out as + // `(data_address, metadata)` -- exactly `FatPtr`. + // 2. For a struct `RcInner` whose trailing field is the unsized + // `U`, the pointer metadata of `*const RcInner` is identical to + // the metadata of `*const U` (same vtable / slice length). Building + // a fat pointer from the `RcInner` base address plus `U`'s metadata + // therefore yields a valid `*mut RcInner` for the same + // allocation, which is what this function does. + // + // The `const` block makes assumption (1) a per-instantiation + // compile-time check: if `into_dyn` is ever instantiated with a `U` + // that is not a two-word fat-pointer type (e.g. a `Sized` `U`, whose + // pointer is thin), the reinterpretation would be wrong and compilation + // fails here instead of miscompiling silently. `RcInner` is checked + // as well to pin assumption (2)'s width. + const { + let word = core::mem::size_of::<*const ()>(); + assert!( + core::mem::size_of::<*const U>() == 2 * word, + "Rc::into_dyn requires `U` to be an unsized type whose pointer \ + is a two-word fat pointer (trait object or slice)" + ); + assert!( + core::mem::size_of::<*const RcInner>() == 2 * word, + "Rc::into_dyn requires `RcInner` to be a two-word fat pointer" + ); + } + unsafe { let inner_ptr = self.ptr.as_ptr(); diff --git a/src/util/vec.rs b/src/util/vec.rs index 7e75ba0..54c8e53 100644 --- a/src/util/vec.rs +++ b/src/util/vec.rs @@ -18,12 +18,18 @@ pub struct Vec { } impl Vec { - pub fn from_exact_iter(iter: impl ExactSizeIterator) -> Self { - let mut o = Self::new(iter.len() as u32).unwrap(); + /// Build a `Vec` from an exact-size iterator. + /// + /// The capacity is taken from `iter.len()`. Returns [`AllocError`] if that + /// length does not fit in a `u32` (instead of silently truncating the cast) + /// or if allocating the backing buffer fails. + pub fn from_exact_iter(iter: impl ExactSizeIterator) -> Result { + let capacity = u32::try_from(iter.len()).map_err(|_| AllocError::AllocationFailed)?; + let mut o = Self::new(capacity)?; for i in iter { o.push(i); } - o + Ok(o) } } @@ -49,24 +55,35 @@ impl core::fmt::Debug for Vec { } } -impl Clone for Vec { - fn clone(&self) -> Self { - let mut n: Vec = Vec::new_in(self.alloc.clone(), self.inner.capacity).unwrap(); - - if !self.is_empty() { - // SAFETY: We need to write to uninitialized memory without creating a reference to it. - // Use ptr::write to initialize each element. - unsafe { - for i in 0..self.len() { - core::ptr::write(n.inner.ptr.add(i), self[i].clone()); - } - } +impl Vec { + /// Attempt to clone the vector, returning [`AllocError`] if allocating the + /// new backing buffer fails. The clone preserves both length and capacity. + /// + /// This is panic-safe: if an element's [`Clone`] implementation panics + /// part-way through, every element cloned so far is dropped before the + /// panic propagates, so nothing is leaked. The guard is the new `Vec`'s own + /// [`Drop`] — [`Vec::push`] advances its `len` only *after* each element is + /// written, and on unwind `Drop` reclaims exactly that initialized prefix. + pub fn try_clone(&self) -> Result { + let mut n: Vec = Vec::new_in(self.alloc.clone(), self.inner.capacity)?; + + for i in 0..self.len() { + // `self.len() <= capacity == n.capacity`, so `push` never overflows. + // `self[i].clone()` is evaluated before `push` takes ownership, so a + // panic there leaves `n` holding only the already-cloned prefix. + n.push(self[i].clone()); } - // Only set len after initializing the memory - n.inner.len = self.inner.len; + Ok(n) + } +} - n +impl Clone for Vec { + /// # Panics + /// Panics if allocating the new backing buffer fails. Use + /// [`Vec::try_clone`] to handle allocation failure without panicking. + fn clone(&self) -> Self { + self.try_clone().expect("Vec::clone: allocation failed") } } @@ -92,26 +109,46 @@ impl Vec { Vec::new_in(GlobalAllocator, capacity) } - pub fn new_from(ptr: *mut T, capacity: u32) -> Vec { + /// Build a `Vec` that adopts a raw allocation, using [`GlobalAllocator`]. + /// + /// The returned `Vec` starts empty (`len == 0`) and treats all `capacity` + /// slots as uninitialized. Its [`Drop`] frees `ptr` with [`GlobalAllocator`]. + /// + /// # Safety + /// - `ptr` must point to a single allocation of at least `capacity` values + /// of `T`, produced by [`GlobalAllocator`] with the layout + /// `Layout::array::(capacity)` (correctly sized and aligned). + /// - Ownership of that allocation is transferred to the returned `Vec`: it + /// must not be freed, aliased, or otherwise used elsewhere, since the + /// `Vec`'s `Drop` will free it with the same allocator and layout. + pub unsafe fn new_from(ptr: *mut T, capacity: u32) -> Vec { Vec { - inner: InnerVec { - ptr, - capacity, - len: 0, - }, + // SAFETY: guaranteed by this function's contract; `len == 0` so no + // element is claimed initialized. + inner: unsafe { InnerVec::from_raw_parts(ptr, capacity, 0) }, alloc: GlobalAllocator, } } } impl Vec { - pub fn new_from_with_alloc(ptr: *mut T, capacity: u32, alloc: A) -> Vec { + /// Build a `Vec` that adopts a raw allocation, using a custom allocator. + /// + /// The returned `Vec` starts empty (`len == 0`) and treats all `capacity` + /// slots as uninitialized. Its [`Drop`] frees `ptr` with `alloc`. + /// + /// # Safety + /// - `ptr` must point to a single allocation of at least `capacity` values + /// of `T`, produced by `alloc` with the layout + /// `Layout::array::(capacity)` (correctly sized and aligned). + /// - Ownership of that allocation is transferred to the returned `Vec`: it + /// must not be freed, aliased, or otherwise used elsewhere, since the + /// `Vec`'s `Drop` will free it with `alloc` and the same layout. + pub unsafe fn new_from_with_alloc(ptr: *mut T, capacity: u32, alloc: A) -> Vec { Vec { - inner: InnerVec { - ptr, - capacity, - len: 0, - }, + // SAFETY: guaranteed by this function's contract; `len == 0` so no + // element is claimed initialized. + inner: unsafe { InnerVec::from_raw_parts(ptr, capacity, 0) }, alloc, } } @@ -131,11 +168,10 @@ impl Vec { }; Ok(Vec { - inner: InnerVec { - ptr: ptr as *mut T, - capacity, - len: 0, - }, + // SAFETY: `ptr` is either null (capacity 0) or a fresh allocation of + // `capacity` elements from `alloc`, and `len == 0` so no element is + // claimed initialized. + inner: unsafe { InnerVec::from_raw_parts(ptr as *mut T, capacity, 0) }, alloc, }) } @@ -144,11 +180,7 @@ impl Vec { impl Vec { pub fn zero() -> Vec { Vec { - inner: InnerVec { - ptr: core::ptr::null_mut(), - capacity: 0, - len: 0, - }, + inner: InnerVec::zero(), alloc: GlobalAllocator, } } @@ -167,7 +199,11 @@ impl Vec { self.inner.capacity() } - /// Appends an element to the back of a collection. + /// Appends an element to the back of the vector. + /// + /// Unlike `alloc::vec::Vec`, this vector has a fixed capacity fixed at + /// construction and never reallocates. Use [`Vec::try_push`] to handle a + /// full vector without panicking. /// /// # Panics /// @@ -176,9 +212,25 @@ impl Vec { /// # Examples /// /// ``` - /// let mut vec = vec![1, 2]; + /// # // `spacewasm` is `no_std`: its `GlobalAllocator` forwards to the + /// # // `__spacewasm_*` symbols an embedder installs via `global_allocator!`. + /// # // A doctest is its own binary, so provide a std-backed implementation. + /// # struct DocAlloc; + /// # unsafe impl spacewasm::Allocator for DocAlloc { + /// # unsafe fn alloc(&self, l: core::alloc::Layout) -> Result<*mut u8, spacewasm::AllocError> { + /// # Ok(unsafe { std::alloc::alloc(l) }) + /// # } + /// # unsafe fn dealloc(&self, p: *mut u8, l: core::alloc::Layout) { unsafe { std::alloc::dealloc(p, l) } } + /// # fn memory_statistics(&self) -> spacewasm::MemoryStatistics { spacewasm::MemoryStatistics { total_bytes: 0, pad_bytes: 0 } } + /// # } + /// # spacewasm::global_allocator!(DocAlloc, DocAlloc); + /// use spacewasm::Vec; + /// + /// let mut vec = Vec::new(3).unwrap(); + /// vec.push(1); + /// vec.push(2); /// vec.push(3); - /// assert_eq!(vec, [1, 2, 3]); + /// assert_eq!(&vec[..], &[1, 2, 3]); /// ``` /// /// # Time complexity @@ -192,15 +244,29 @@ impl Vec { self.inner.try_push(value) } - /// Removes the last element from a vector and returns it, or [`None`] if it - /// is empty. + /// Removes the last element from the vector and returns it, or [`None`] if + /// it is empty. The capacity is unchanged. /// /// # Examples /// /// ``` - /// let mut vec = vec![1, 2, 3]; + /// # // `spacewasm` is `no_std`: its `GlobalAllocator` forwards to the + /// # // `__spacewasm_*` symbols an embedder installs via `global_allocator!`. + /// # // A doctest is its own binary, so provide a std-backed implementation. + /// # struct DocAlloc; + /// # unsafe impl spacewasm::Allocator for DocAlloc { + /// # unsafe fn alloc(&self, l: core::alloc::Layout) -> Result<*mut u8, spacewasm::AllocError> { + /// # Ok(unsafe { std::alloc::alloc(l) }) + /// # } + /// # unsafe fn dealloc(&self, p: *mut u8, l: core::alloc::Layout) { unsafe { std::alloc::dealloc(p, l) } } + /// # fn memory_statistics(&self) -> spacewasm::MemoryStatistics { spacewasm::MemoryStatistics { total_bytes: 0, pad_bytes: 0 } } + /// # } + /// # spacewasm::global_allocator!(DocAlloc, DocAlloc); + /// use spacewasm::Vec; + /// + /// let mut vec = Vec::from_array([1, 2, 3]).unwrap(); /// assert_eq!(vec.pop(), Some(3)); - /// assert_eq!(vec, [1, 2]); + /// assert_eq!(&vec[..], &[1, 2]); /// ``` /// /// # Time complexity @@ -274,10 +340,10 @@ impl Drop for Vec { } } -impl IntoIterator for Vec { +impl IntoIterator for Vec { type Item = T; - type IntoIter = IntoIter; - fn into_iter(self) -> IntoIter { + type IntoIter = IntoIter; + fn into_iter(self) -> IntoIter { // Make sure not to drop Vec since that would free the buffer let vec = core::mem::ManuallyDrop::new(self); @@ -286,6 +352,9 @@ impl IntoIterator for Vec { let cap = vec.inner.capacity as usize; let len = vec.inner.len as usize; + // SAFETY: move the allocator out of the `ManuallyDrop` wrapper into the IntoIter + let alloc = unsafe { core::ptr::read(&vec.alloc) }; + IntoIter { buf: ptr, cap, @@ -296,7 +365,7 @@ impl IntoIterator for Vec { } else { unsafe { ptr.add(len) } }, - alloc: GlobalAllocator, + alloc, } } } @@ -410,7 +479,8 @@ mod kani_proofs { } /// Verify that Vec::clone creates an independent copy with equal contents. - /// Tests the unsafe ptr::write loop in Clone::clone (vec.rs:63-82). + /// Exercises the clone path (`Clone::clone` -> `try_clone`), which pushes + /// each cloned element into a freshly allocated buffer. #[kani::proof] #[kani::unwind(4)] fn proof_vec_clone_correctness() { @@ -523,6 +593,42 @@ mod tests { vec.push(3); } + #[test] + fn test_try_push() { + let mut vec = Vec::new(2).unwrap(); + + // Succeeds while there is spare capacity. + assert_eq!(vec.try_push(1), Ok(())); + assert_eq!(vec.try_push(2), Ok(())); + assert_eq!(vec.len(), 2); + + // Once full, try_push reports the failure instead of panicking. + assert_eq!(vec.try_push(3), Err(AllocError::OutOfMemory)); + assert_eq!(vec.len(), 2); + assert_eq!(&vec[..], &[1, 2]); + } + + #[test] + fn test_into_boxed_slice() { + let mut vec = Vec::new(3).unwrap(); + vec.push(10); + vec.push(20); + vec.push(30); + + let boxed = vec.into_boxed_slice(); + assert_eq!(boxed.len(), 3); + assert_eq!(&*boxed, &[10, 20, 30]); + } + + #[test] + #[should_panic] + fn test_into_boxed_slice_not_full_panics() { + // into_boxed_slice requires len == capacity. + let mut vec = Vec::new(3).unwrap(); + vec.push(1); + let _ = vec.into_boxed_slice(); + } + #[test] fn test_deref() { let mut vec = Vec::new(3).unwrap(); @@ -665,7 +771,7 @@ mod tests { #[test] fn test_from_exact_iter() { - let vec = Vec::from_exact_iter([10, 20, 30].into_iter()); + let vec = Vec::from_exact_iter([10, 20, 30].into_iter()).unwrap(); assert_eq!(vec.len(), 3); assert_eq!(vec.capacity(), 3); assert_eq!(&vec[..], &[10, 20, 30]); @@ -698,7 +804,9 @@ mod tests { let layout = Layout::array::(capacity as usize).unwrap(); let ptr = unsafe { GlobalAllocator.alloc(layout).unwrap() as *mut i32 }; - let mut vec = Vec::new_from(ptr, capacity); + // SAFETY: `ptr` is a fresh GlobalAllocator allocation for `capacity` + // i32s and ownership is transferred to the returned Vec. + let mut vec = unsafe { Vec::new_from(ptr, capacity) }; assert_eq!(vec.capacity(), 3); assert_eq!(vec.len(), 0); @@ -716,7 +824,9 @@ mod tests { let layout = Layout::array::(capacity as usize).unwrap(); let ptr = unsafe { RustSystemAllocator.alloc(layout).unwrap() as *mut i32 }; - let mut vec = Vec::new_from_with_alloc(ptr, capacity, RustSystemAllocator); + // SAFETY: `ptr` is a fresh RustSystemAllocator allocation for `capacity` + // i32s and ownership is transferred to the returned Vec. + let mut vec = unsafe { Vec::new_from_with_alloc(ptr, capacity, RustSystemAllocator) }; assert_eq!(vec.capacity(), 2); assert_eq!(vec.len(), 0); diff --git a/tests/statistics_integration.rs b/tests/statistics_integration.rs deleted file mode 100644 index 1a13a97..0000000 --- a/tests/statistics_integration.rs +++ /dev/null @@ -1,267 +0,0 @@ -//! Integration test for [`spacewasm::Module::new_with_statistics`]. -//! -//! `new_with_statistics` decodes a module while sampling the global allocator's -//! live-byte total before and after each section, attributing the delta to that -//! section. This test installs a tracking global allocator, decodes a -//! hard-coded module, and asserts that the reported per-section usage is -//! self-consistent and lands in the sections we expect to allocate. - -use core::alloc::Layout; -use core::ptr::NonNull; -use core::sync::atomic::{AtomicI32, Ordering}; - -use spacewasm::{ - AllocError, Allocator, CodeBuilder, CompilerOptions, Engine, InnerVec, MemoryStatistics, - Module, SectionKind, WasmMemoryAllocator, WasmStream, global_allocator, vec, -}; - -extern crate std; -use std::sync::Mutex; - -// `LIVE_BYTES` is a single process-wide counter shared by every test in this -// binary. Tests in a binary run concurrently by default, so a second test -// allocating/freeing through the global allocator would corrupt the -// before/after deltas the statistics test samples per section (observed as a -// spurious negative delta on CI). Serialize the tests that touch the global -// allocator so each one measures in isolation. -static ALLOC_LOCK: Mutex<()> = Mutex::new(()); - -// --------------------------------------------------------------------------- -// Tracking global allocator -// -// The statistics machinery reads `GlobalAllocator::memory_statistics()`, which -// dispatches to the `__spacewasm_*` symbols defined by `global_allocator!`. -// We back it with the system heap and keep a running tally of live bytes so -// the reported deltas are non-zero and meaningful. -// --------------------------------------------------------------------------- - -static LIVE_BYTES: AtomicI32 = AtomicI32::new(0); - -struct TrackingAllocator; - -unsafe impl Allocator for TrackingAllocator { - unsafe fn alloc(&self, layout: Layout) -> Result<*mut u8, AllocError> { - let ptr = unsafe { std::alloc::alloc(layout) }; - if ptr.is_null() { - Err(AllocError::AllocationFailed) - } else { - LIVE_BYTES.fetch_add(layout.size() as i32, Ordering::SeqCst); - Ok(ptr) - } - } - - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - LIVE_BYTES.fetch_sub(layout.size() as i32, Ordering::SeqCst); - unsafe { std::alloc::dealloc(ptr, layout) } - } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: LIVE_BYTES.load(Ordering::SeqCst), - pad_bytes: 0, - } - } -} - -// Also usable as the linear-memory allocator argument. The module under test -// declares no memory, so these are never exercised, but the argument still -// needs a concrete `WasmMemoryAllocator`. -impl WasmMemoryAllocator for TrackingAllocator { - fn allocate(&self, layout: Layout) -> Result, AllocError> { - unsafe { NonNull::new(std::alloc::alloc(layout)).ok_or(AllocError::AllocationFailed) } - } - - fn reallocate( - &self, - ptr: NonNull, - old_layout: Layout, - layout: Layout, - ) -> Result, AllocError> { - unsafe { - NonNull::new(std::alloc::realloc(ptr.as_ptr(), old_layout, layout.size())) - .ok_or(AllocError::AllocationFailed) - } - } - - fn deallocate(&self, ptr: NonNull, layout: Layout) { - unsafe { std::alloc::dealloc(ptr.as_ptr(), layout) } - } -} - -global_allocator!(TrackingAllocator, TrackingAllocator); - -// --------------------------------------------------------------------------- -// A single-shot stream over an in-memory byte buffer. -// --------------------------------------------------------------------------- - -struct ByteStream { - buffer: std::vec::Vec, - consumed: bool, -} - -impl ByteStream { - fn new(data: &[u8]) -> Self { - ByteStream { - buffer: data.to_vec(), - consumed: false, - } - } -} - -impl WasmStream for ByteStream { - fn read(&mut self) -> Result>, u8> { - if self.consumed { - return Ok(None); - } - self.consumed = true; - Ok(Some(InnerVec { - ptr: self.buffer.as_mut_ptr(), - capacity: self.buffer.len() as u32, - len: self.buffer.len() as u32, - })) - } - - fn return_(&mut self, _chunk: InnerVec) { - // The buffer is owned by `self`; nothing to reclaim. - } -} - -/// A hand-assembled module with two function types, two functions, a code -/// section, and two exports: -/// -/// ```wat -/// (module -/// (type (func (param i32 i32) (result i32))) -/// (type (func (result i32))) -/// (func (type 0) (local.get 0) (local.get 1) (i32.add)) -/// (func (type 1) (i32.const 42)) -/// (export "add" (func 0)) -/// (export "answer" (func 1))) -/// ``` -#[rustfmt::skip] -static STAT_WASM: &[u8] = &[ - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, - // type section (id 1) - 0x01, 0x0b, 0x02, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x60, 0x00, 0x01, 0x7f, - // function section (id 3) - 0x03, 0x03, 0x02, 0x00, 0x01, - // export section (id 7) - 0x07, 0x10, 0x02, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x06, 0x61, 0x6e, 0x73, - 0x77, 0x65, 0x72, 0x00, 0x01, - // code section (id 10) - 0x0a, 0x0e, 0x02, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b, 0x04, 0x00, - 0x41, 0x2a, 0x0b, -]; - -const MAX_CONTROL_FRAMES: usize = 128; -const MAX_STACK_DEPTH: usize = 256; -const MAX_CODE_PAGES: u32 = 256; - -/// Compiler options used to build the code builders in these tests. -fn options() -> CompilerOptions { - CompilerOptions { - allow_memory_grow: false, - max_backpatch_iterations: 0, - max_code_pages: MAX_CODE_PAGES, - } -} - -#[test] -fn new_with_statistics_reports_per_section_usage() { - let _guard = ALLOC_LOCK.lock().unwrap(); - - let mut engine = Engine::new(1024, 8, vec![]).unwrap(); - let mut code_builder = CodeBuilder::new(options()).unwrap(); - let mut stream = ByteStream::new(STAT_WASM); - - let allocator = spacewasm::Rc::new(TrackingAllocator) - .unwrap() - .into_wasm_memory_allocator(); - - let (module, stats) = Module::new_with_statistics::( - "stats", - &mut stream, - &mut engine.store, - &mut code_builder, - allocator, - ) - .expect("module should decode"); - - // The module decoded to the expected shape. - assert_eq!(module.functions.len(), 2); - assert_eq!(module.types.len(), 2); - assert_eq!(module.exports.len(), 2); - - // The statistics array is indexed by `SectionKind`. - assert_eq!(stats.len(), SectionKind::N as usize); - - // The type section decodes into a `Vec` of function types, so it must - // register a positive live-byte delta. - let type_bytes = stats[SectionKind::Type as usize].total_bytes; - assert!( - type_bytes > 0, - "type section should allocate, got {type_bytes}" - ); - - // Likewise the function and export sections build owned collections. - let function_bytes = stats[SectionKind::Function as usize].total_bytes; - assert!( - function_bytes > 0, - "function section should allocate, got {function_bytes}" - ); - let export_bytes = stats[SectionKind::Export as usize].total_bytes; - assert!( - export_bytes > 0, - "export section should allocate, got {export_bytes}" - ); - - // Sections absent from the module were never sampled and stay at zero. - assert_eq!(stats[SectionKind::Import as usize].total_bytes, 0); - assert_eq!(stats[SectionKind::Memory as usize].total_bytes, 0); - assert_eq!(stats[SectionKind::Global as usize].total_bytes, 0); - assert_eq!(stats[SectionKind::Table as usize].total_bytes, 0); - assert_eq!(stats[SectionKind::Data as usize].total_bytes, 0); - assert_eq!(stats[SectionKind::Element as usize].total_bytes, 0); - assert_eq!(stats[SectionKind::Start as usize].total_bytes, 0); -} - -#[test] -fn new_with_statistics_matches_plain_new() { - let _guard = ALLOC_LOCK.lock().unwrap(); - - // Decoding the same bytes with and without statistics must produce an - // equivalent module; the statistics variant only adds sampling. - let allocator = || { - spacewasm::Rc::new(TrackingAllocator) - .unwrap() - .into_wasm_memory_allocator() - }; - - let mut engine_a = Engine::new(1024, 8, vec![]).unwrap(); - let mut cb_a = CodeBuilder::new(options()).unwrap(); - let mut stream_a = ByteStream::new(STAT_WASM); - let plain = Module::new::( - "plain", - &mut stream_a, - &mut engine_a.store, - &mut cb_a, - allocator(), - ) - .unwrap(); - - let mut engine_b = Engine::new(1024, 8, vec![]).unwrap(); - let mut cb_b = CodeBuilder::new(options()).unwrap(); - let mut stream_b = ByteStream::new(STAT_WASM); - let (with_stats, _stats) = Module::new_with_statistics::( - "with-stats", - &mut stream_b, - &mut engine_b.store, - &mut cb_b, - allocator(), - ) - .unwrap(); - - assert_eq!(plain.functions.len(), with_stats.functions.len()); - assert_eq!(plain.types.len(), with_stats.types.len()); - assert_eq!(plain.exports.len(), with_stats.exports.len()); -} diff --git a/tests/util/spectest.rs b/tests/util/spectest.rs index 42e0643..808afbe 100644 --- a/tests/util/spectest.rs +++ b/tests/util/spectest.rs @@ -18,9 +18,9 @@ use serde::{Deserialize, Serialize}; use spacewasm::{ AllocError, Allocator, CodeBuilder, CompilerOptions, ConstantExprError, Engine, ExportDesc, GlobalValue, GlobalValueError, HostFunction, HostGlobal, HostModule, InnerVec, Interpreter, - InterpreterResult, InterpreterRunner, InvokeError, Limit, Memory, MemoryError, - MemoryStatistics, Module, ModuleRef, ParseError, Ref, TrapReason, ValType, ValidationError, - Value, WasmMemoryAllocator, WasmRef, WasmStream, global_allocator, vec, + InterpreterResult, InterpreterRunner, InvokeError, Limit, Memory, MemoryError, Module, + ModuleRef, ParseError, Ref, TrapReason, ValType, ValidationError, Value, WasmMemoryAllocator, + WasmRef, WasmStream, global_allocator, vec, }; use std::alloc::Layout; use std::cell::RefCell; @@ -198,13 +198,6 @@ unsafe impl Allocator for SpecTestAllocator { unsafe { std::alloc::dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: 0, - pad_bytes: 0, - } - } } struct RustSystemAllocator; @@ -252,9 +245,8 @@ struct StaticGlobal { } impl GlobalValue for StaticGlobal { - fn write(&self, value: Value) -> Result<(), GlobalValueError> { - *self.value.lock().unwrap() = value; - Ok(()) + fn write(&self, _value: Value) -> Result<(), GlobalValueError> { + Err(GlobalValueError) } fn read(&self) -> Result { @@ -302,10 +294,13 @@ impl WasmStream for ByteStream { if let Some(ref mut vec) = self.buffer { self.consumed = true; - let inner = InnerVec { - ptr: vec.as_mut_ptr(), - capacity: vec.len() as u32, - len: vec.len() as u32, + debug_assert!( + u32::try_from(vec.len()).is_ok(), + "wasm module length {} does not fit in u32", + vec.len() + ); + let inner = unsafe { + InnerVec::from_raw_parts(vec.as_mut_ptr(), vec.len() as u32, vec.len() as u32) }; Ok(Some(inner)) } else { @@ -314,7 +309,8 @@ impl WasmStream for ByteStream { } fn return_(&mut self, _chunk: InnerVec) { - // Buffer is kept alive in self.buffer, so nothing to do + // No-op: the stream owns `self.buffer` and never handed off ownership, + // so there is nothing to reclaim. See the handoff rationale in `read`. } } @@ -352,7 +348,7 @@ impl TestContext { engine: new_engine(host_modules), code_builder: CodeBuilder::new(CompilerOptions { allow_memory_grow: true, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: MAX_CODE_PAGES, }) .unwrap(), @@ -391,7 +387,7 @@ impl TestContext { // Clone all modules into the new store for module in self.engine.store.modules().iter() { let cloned_module = clone_module(module); - cloned.store.push_module(cloned_module); + cloned.store.push_module(cloned_module).unwrap(); } cloned @@ -602,9 +598,12 @@ fn clone_memory(memory: &Memory) -> spacewasm::Rc { } } - // Copy the memory contents + // Copy the memory contents. Use the memory's actual page size rather than + // assuming the default 64 KiB Wasm page, so memories declared under the + // custom-page-sizes proposal (e.g. `MemPageSize::_1`) are cloned with the + // correct byte length instead of over-/under-reading. if current_size > 0 { - let size_in_bytes = (current_size as usize) * 65536; + let size_in_bytes = (current_size as usize) * mem_type.page_size(); let data = memory.load(0, size_in_bytes).unwrap(); new_memory.store(0, data).unwrap(); } @@ -691,7 +690,9 @@ fn load_module( match ctx.engine.module_start(module_ref) { None => InterpreterResult::Finished, Some(start) => match ctx.engine.invoke(start, &[]) { - Ok(()) => Interpreter.run(ctx.code_builder.pages(), &mut ctx.engine, usize::MAX), + Ok(()) => { + Interpreter.run(ctx.code_builder.pages(), &mut ctx.engine, MAX_INVOKE_FUEL) + } Err(InvokeError::StackOverflow) => { InterpreterResult::Trap(TrapReason::StackOverflow) } @@ -749,7 +750,9 @@ fn invoke_function_resume( // deallocation may occur. let result = { let _locked = enter_locked(); - ctx.engine.resume(resume_value); + ctx.engine + .resume(resume_value) + .expect("engine should be paused when resuming"); test_runner.run(ctx.code_builder.pages(), &mut ctx.engine, MAX_INVOKE_FUEL) }; @@ -944,56 +947,96 @@ fn check_trap_reason(reason: TrapReason, text: &str) { } } +/// Assert that an interpreter [`ValidationError`] is an acceptable match for the +/// spec-suite's expected rejection `text`. fn check_decode_error(err: ParseError, text: String) { match (err.err.err, text.as_str()) { + // --- Encoding / structural malformations --- + (ValidationError::MalformedMagic, "magic header not detected") => {} + (ValidationError::MalformedVersion, "unknown binary version") => {} + (ValidationError::MalformedUtf8, "malformed UTF-8 encoding") => {} + (ValidationError::MalformedSectionId(_), "malformed section id") => {} + ( + ValidationError::MalformedSectionSize, + "section size mismatch" | "unexpected end" | "malformed value type", + ) => {} + ( + ValidationError::InvalidCodeSectionFunctionCount, + "function and code section have inconsistent lengths", + ) => {} + (ValidationError::DuplicateSection(_), "unexpected content after last section") => {} + (ValidationError::InvalidSectionOrdering(_, _), "unexpected section order") => {} + (ValidationError::ExpectedTerminal(0), "zero byte expected") => {} ( ValidationError::MalformedInteger, "integer too large" | "integer representation too long", ) => {} - (ValidationError::MalformedMagic, "magic header not detected") => {} - (ValidationError::MalformedVersion, "unknown binary version") => {} - (ValidationError::ExpectedTerminal(0), "zero byte expected") => {} + // A truncated stream ( ValidationError::Eof, - "unexpected end" | "length out of bounds" | "unexpected end of section or function", + "unexpected end" + | "length out of bounds" + | "unexpected end of section or function" + | "integer representation too long" + | "malformed value type", ) => {} - (ValidationError::TooManyLocals, "too many locals") => {} - (ValidationError::MalformedUtf8, "malformed UTF-8 encoding") => {} + (ValidationError::VecTooLong, "length out of bounds") => {} + + // --- Value / type descriptors --- + (ValidationError::MalformedValueType(_), "malformed value type") => {} + (ValidationError::MalformedFunction(_), "malformed function type") => {} + (ValidationError::MalformedElemType(_), "malformed element type") => {} + (ValidationError::MalformedLimit(_), "malformed limits flag") => {} + (ValidationError::MalformedMemType(_), "malformed memory type") => {} + // Import and export descriptors share the same malformed-kind variant. ( - ValidationError::InvalidCodeSectionFunctionCount, - "function and code section have inconsistent lengths", + ValidationError::MalformedImportExportDesc(_), + "malformed import kind" | "malformed export kind", ) => {} - (ValidationError::MalformedSectionSize, "section size mismatch") => {} - (ValidationError::LocalIdxOutOfRange, "unknown local") => {} - (ValidationError::MultipleMemories, "multiple memories") => {} - (ValidationError::AlignmentLargerThanType, "alignment must not be larger than natural") => { - } - (ValidationError::TypeMismatch, "type mismatch") => {} - (ValidationError::BlockResultTypeMismatch, "type mismatch") => {} - (ValidationError::InvalidElseBlock, "else without matching if") => {} - (ValidationError::InvalidLabelIndex, "unknown label") => {} - (ValidationError::MalformedSectionSize, "unexpected end") => {} + (ValidationError::ExpectedConstOrVar(_), "malformed mutability") => {} + + // --- Index-space / definition lookups ("unknown X" family) --- + (ValidationError::LocalIdxOutOfRange, "unknown local" | "local offset out of range") => {} (ValidationError::GlobalIdxOutOfRange, "unknown global") => {} - (ValidationError::MalformedSectionId(_), "malformed section id") => {} - (ValidationError::VecTooLong, "length out of bounds") => {} - (ValidationError::StackUnderflow, "type mismatch") => {} (ValidationError::TypeIdxOutOfRange, "unknown type") => {} - (ValidationError::FunctionResultTypeMismatch, "type mismatch") => {} + (ValidationError::FunctionImportOutOfRange, "unknown type") => {} (ValidationError::FunctionIdxOutOfRange, "unknown function") => {} - (ValidationError::FunctionReturnsTooLarge, "invalid result arity") => {} (ValidationError::TableNotDefined, "unknown table") => {} - (ValidationError::InvalidTableIndex, "malformed value type") => {} - (ValidationError::InvalidLabelIndex, "unexpected end of section or function") => {} - (ValidationError::MalformedValueType(_), "malformed value type") => {} - (ValidationError::DuplicateSection(_), "unexpected content after last section") => {} - (ValidationError::GlobalIsNotMutable, "immutable global") => {} - (ValidationError::InvalidElementOffset, "type mismatch") => {} + (ValidationError::MemoryNotDefined, "unknown memory") => {} + (ValidationError::InvalidMemIndex, "unknown memory") => {} + (ValidationError::InvalidTableIndex, "malformed value type" | "unknown table") => {} + + // --- Control flow --- + (ValidationError::InvalidElseBlock, "else without matching if") => {} + // An out-of-range label, or a branch whose target is truncated away. + ( + ValidationError::InvalidLabelIndex, + "unknown label" | "unexpected end of section or function", + ) => {} + + // --- Type checking ("type mismatch" is a coarse spec category) --- + (ValidationError::TypeMismatch, "type mismatch") => {} + (ValidationError::StackUnderflow, "type mismatch") => {} + (ValidationError::FunctionResultTypeMismatch, "type mismatch") => {} + (ValidationError::GlobalTypeMismatch, "type mismatch") => {} + (ValidationError::InvalidMemOffsetType, "type mismatch") => {} + // Either a plain block-result mismatch, or a result-typed `if` missing + // its `else` arm (both are the same internal state). + ( + ValidationError::BlockResultTypeMismatch, + "type mismatch" | "result-typed if without else", + ) => {} + (ValidationError::BrTableResultTypeMismatch, "type mismatch") => {} + (ValidationError::FunctionReturnsTooLarge, "invalid result arity") => {} + (ValidationError::TooManyLocals, "too many locals") => {} + (ValidationError::AlignmentLargerThanType, "alignment must not be larger than natural") => { + } + + // --- Constant expressions --- ( ValidationError::InvalidConstantExpr(ConstantExprError::InvalidConstantInstruction), "constant expression required", ) => {} - (ValidationError::FunctionImportOutOfRange, "unknown type") => {} - (ValidationError::GlobalTypeMismatch, "type mismatch") => {} ( ValidationError::InvalidConstantExpr(ConstantExprError::AlreadyHasValue), "type mismatch", @@ -1003,54 +1046,69 @@ fn check_decode_error(err: ParseError, text: String) { ValidationError::InvalidConstantExpr(ConstantExprError::InvalidGlobal), "unknown global", ) => {} - (ValidationError::ExpectedConstOrVar(_), "malformed mutability") => {} - (ValidationError::MemoryNotDefined, "unknown memory") => {} + + // --- Immutable globals --- + // `GlobalNotMutable` covers both a `global.set` on an immutable + // global (`"immutable global"`) and an import whose mutability disagrees + // with the definition (`"incompatible import type"`). + (ValidationError::GlobalNotMutable, "immutable global" | "incompatible import type") => {} + + // --- Limits / sizing --- (ValidationError::InvalidMaxLimit, "size minimum must not be greater than maximum") => {} - (ValidationError::MemoryTooLarge, "memory size must be at most 65536 pages (4GiB)") => {} - (ValidationError::MemoryTooLarge, "memory size must be at most 4 GiB") => {} + // Two upstream phrasings for the same 4 GiB memory cap. + ( + ValidationError::MemoryTooLarge, + "memory size must be at most 65536 pages (4GiB)" | "memory size must be at most 4 GiB", + ) => {} (ValidationError::TableTooLarge, "table size too large") => {} - (ValidationError::LocalIdxOutOfRange, "local offset out of range") => {} - (ValidationError::BlockResultTypeMismatch, "result-typed if without else") => {} - (ValidationError::InvalidNegativeMemOffset, "data segment does not fit") => {} - (ValidationError::InvalidMemOffsetType, "type mismatch") => {} + (ValidationError::InvalidPageSize(_), "invalid custom page size") => {} + (ValidationError::StackTooLarge, "call frame too large") => {} + + // --- Sections / names that must be unique --- + (ValidationError::MultipleMemories, "multiple memories") => {} + (ValidationError::MultipleTables, "multiple tables") => {} + (ValidationError::DuplicateExportName, "duplicate export name") => {} + + // --- Start function --- (ValidationError::InvalidStartFunctionSignature, "start function") => {} ( ValidationError::InvalidHostStartFunction, "start function must not be a host function", ) => {} - (ValidationError::DuplicateExportName, "duplicate export name") => {} - (ValidationError::InvalidTableIndex, "unknown table") => {} + + // --- Data / element segment placement --- + (ValidationError::InvalidNegativeMemOffset, "data segment does not fit") => {} (ValidationError::MemoryError(MemoryError::OutOfBounds), "data segment does not fit") => {} - (ValidationError::InvalidMemIndex, "unknown memory") => {} - (ValidationError::FunctionImportNotFound, "unknown import") => {} - (ValidationError::GlobalImportNotFound, "unknown import") => {} - (ValidationError::MemoryImportNotFound, "unknown import") => {} + (ValidationError::InvalidElementOutOfBounds, "elements segment does not fit") => {} + // A bad element offset is either a type mismatch on the offset + // expression, or an out-of-range placement past the table end. + ( + ValidationError::InvalidElementOffset, + "type mismatch" | "elements segment does not fit", + ) => {} + (ValidationError::TableRefNotUnique, "table reference not unique") => {} + + // --- Imports ("unknown import" vs "incompatible import type") --- + // Each `*ImportNotFound` variant is used both when no matching export + // exists at all (`"unknown import"`) and when a candidate exists but is + // rejected as incompatible (`"incompatible import type"`). + ( + ValidationError::FunctionImportNotFound, + "unknown import" | "incompatible import type", + ) => {} + (ValidationError::GlobalImportNotFound, "unknown import" | "incompatible import type") => {} + (ValidationError::MemoryImportNotFound, "unknown import" | "incompatible import type") => {} + (ValidationError::TableImportNotFound, "unknown import" | "incompatible import type") => {} (ValidationError::FunctionImportTypeMismatch, "incompatible import type") => {} (ValidationError::GlobalImportTypeMismatch, "incompatible import type") => {} (ValidationError::MemoryImportTypeMismatch, "incompatible import type") => {} - (ValidationError::FunctionImportNotFound, "incompatible import type") => {} - (ValidationError::GlobalImportNotFound, "incompatible import type") => {} - (ValidationError::MemoryImportNotFound, "incompatible import type") => {} - (ValidationError::GlobalIsNotMutable, "incompatible import type") => {} - (ValidationError::InvalidElementOutOfBounds, "elements segment does not fit") => {} - (ValidationError::InvalidElementOffset, "elements segment does not fit") => {} - (ValidationError::MultipleTables, "multiple tables") => {} - (ValidationError::TableImportNotFound, "unknown import") => {} + (ValidationError::MemoryImportTooLarge, "incompatible import type") => {} (ValidationError::TableImportIncompatibleSize, "incompatible import type") => {} (ValidationError::TableImportTypeMismatch, "incompatible import type") => {} - (ValidationError::TableImportNotFound, "incompatible import type") => {} - (ValidationError::MemoryImportTooLarge, "incompatible import type") => {} - (ValidationError::InvalidPageSize(_), "invalid custom page size") => {} + + // --- Resource allocation --- (ValidationError::GuestMemoryAllocationFailure, "allocation failed") => {} - (ValidationError::MalformedFunction(_), "malformed function type") => {} - (ValidationError::MalformedElemType(_), "malformed element type") => {} - (ValidationError::MalformedLimit(_), "malformed limits flag") => {} - (ValidationError::MalformedMemType(_), "malformed memory type") => {} - (ValidationError::MalformedImportExportDesc(_), "malformed import kind") => {} - (ValidationError::MalformedImportExportDesc(_), "malformed export kind") => {} - (ValidationError::InvalidSectionOrdering(_, _), "unexpected section order") => {} - (ValidationError::StackTooLarge, "call frame too large") => {} - (ValidationError::TableRefNotUnique, "table reference not unique") => {} + err => { panic!("Could not match validation error text '{text}' with error {err:?}") } @@ -1462,15 +1520,20 @@ pub fn run_wast_test_file(test_name: &str, host_modules: HostModuleFactory) { .to_string_lossy() .to_string(); + // Resolve the WABT `wast2json` tool used to compile the `.wast` + let wast2json = std::env::var_os("WABT_WAST2JSON") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("wast2json")); + // Run wast2json to generate Wasm modules and JSON descriptor - let output = ProcessCommand::new("wast2json") + let output = ProcessCommand::new(&wast2json) .arg(&source_wast_path) .arg("--enable-custom-page-sizes") .arg("-o") .arg(temp_path.join(format!("{}.json", test_filename))) .current_dir(temp_path) .output() - .unwrap_or_else(|e| panic!("Failed to run wast2json: {e}")); + .unwrap_or_else(|e| panic!("Failed to run {}: {e}", wast2json.display())); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); diff --git a/tests/validation_rejections_integration.rs b/tests/validation_rejections_integration.rs index 1370362..2757878 100644 --- a/tests/validation_rejections_integration.rs +++ b/tests/validation_rejections_integration.rs @@ -6,8 +6,8 @@ use core::alloc::Layout; use core::ptr::NonNull; use spacewasm::{ - AllocError, Allocator, CodeBuilder, CompilerOptions, Engine, InnerVec, MemoryStatistics, - Module, ValidationError, WasmMemoryAllocator, WasmStream, global_allocator, + AllocError, Allocator, CodeBuilder, CompilerOptions, Engine, InnerVec, Module, ValidationError, + WasmMemoryAllocator, WasmStream, global_allocator, }; extern crate std; @@ -30,13 +30,6 @@ unsafe impl Allocator for SystemAllocator { unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { std::alloc::dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> MemoryStatistics { - MemoryStatistics { - total_bytes: 0, - pad_bytes: 0, - } - } } global_allocator!(SystemAllocator, SystemAllocator); @@ -47,7 +40,7 @@ fn code_builder_rejects_oversized_max_code_pages() { // `CodeBuilder::new` must return an allocation error rather than panic. let opts = CompilerOptions { allow_memory_grow: false, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: 1 << 24, }; assert!(matches!( @@ -58,7 +51,7 @@ fn code_builder_rejects_oversized_max_code_pages() { // One below the limit still builds successfully, pinning the boundary. let opts_ok = CompilerOptions { allow_memory_grow: false, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: (1 << 24) - 1, }; assert!(CodeBuilder::new(opts_ok).is_ok()); @@ -67,34 +60,34 @@ fn code_builder_rejects_oversized_max_code_pages() { const MAX_CONTROL_FRAMES: usize = 128; const MAX_STACK_DEPTH: usize = 256; -/// Single-shot [`WasmStream`] that hands the whole borrowed buffer over once, -/// then reports EOF. Mirrors `tests/util/spectest.rs::ByteStream` but borrows -/// its bytes (the backing array is a local in each test and outlives the read). -struct ByteStream<'a> { - data: &'a [u8], +/// Single-shot [`WasmStream`] that hands its whole buffer over once, then +/// reports EOF. +struct ByteStream { + buffer: Vec, consumed: bool, } -impl<'a> ByteStream<'a> { - fn new(data: &'a [u8]) -> Self { +impl ByteStream { + fn new(data: &[u8]) -> Self { Self { - data, + buffer: data.to_vec(), consumed: false, } } } -impl WasmStream for ByteStream<'_> { +impl WasmStream for ByteStream { fn read(&mut self) -> Result>, u8> { if self.consumed { return Ok(None); } self.consumed = true; - // SAFETY: `data` outlives this stream; the reader only reads the bytes. - Ok(Some(InnerVec { - ptr: self.data.as_ptr() as *mut u8, - capacity: self.data.len() as u32, - len: self.data.len() as u32, + Ok(Some(unsafe { + InnerVec::from_raw_parts( + self.buffer.as_mut_ptr(), + self.buffer.len() as u32, + self.buffer.len() as u32, + ) })) } @@ -152,7 +145,7 @@ fn compile(wasm: &[u8], options: CompilerOptions) -> Result<(), ValidationError> .map_err(|e| e.err.err) } -/// MF-21: with `allow_memory_grow = false`, a module containing `memory.grow` +/// With `allow_memory_grow = false`, a module containing `memory.grow` /// must be rejected with `IllegalMemoryGrow`; with the flag enabled the same /// bytes compile. The harness (`.wast`) fixes the flag to `true`, so this is /// the only place the rejection can be exercised. @@ -174,7 +167,7 @@ fn memory_grow_rejected_when_option_disabled() { let disabled = CompilerOptions { allow_memory_grow: false, - max_backpatch_iterations: 0, + max_backpatch_iterations: None, max_code_pages: 256, }; assert_eq!( @@ -191,10 +184,9 @@ fn memory_grow_rejected_when_option_disabled() { assert!(compile(WASM, enabled).is_ok()); } -/// MF-22: a non-zero `max_backpatch_iterations` must reject a module whose -/// forward-branch backpatch chain is longer than the limit, with +/// Must reject a module whose forward-branch backpatch chain is longer than the limit, with /// `PossibleBackpatchCycle` — even though the module contains no real cycle. -/// With `max_backpatch_iterations = 0` (unlimited, the harness default) the +/// With `max_backpatch_iterations = None` (unlimited, the harness default) the /// same module compiles. #[test] fn backpatch_chain_exceeding_limit_is_rejected() { @@ -219,7 +211,7 @@ fn backpatch_chain_exceeding_limit_is_rejected() { // A chain of 4 comfortably exceeds a limit of 1. let limited = CompilerOptions { allow_memory_grow: false, - max_backpatch_iterations: 1, + max_backpatch_iterations: Some(1), max_code_pages: 256, }; assert_eq!( @@ -227,10 +219,10 @@ fn backpatch_chain_exceeding_limit_is_rejected() { Err(ValidationError::PossibleBackpatchCycle), ); - // Boundary: `0` disables the limit, so the identical (cycle-free) module + // Boundary: `None` disables the limit, so the identical (cycle-free) module // compiles — proving the rejection comes from the limit, not the bytes. let unlimited = CompilerOptions { - max_backpatch_iterations: 0, + max_backpatch_iterations: None, ..limited }; assert!(compile(WASM, unlimited).is_ok()); From 5fb1b6f6cba20e475f57b2795ecf451b4bb4b0e2 Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Thu, 3 Sep 2026 07:34:48 -0700 Subject: [PATCH 3/7] Noreturn on spacewasm_panic --- crates/spacewasm_c_api/cbindgen.toml | 19 ++++++++ crates/spacewasm_c_api/include/spacewasm.h | 50 ++++++++++++++-------- crates/spacewasm_std/src/main.rs | 43 ++++++++----------- 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/crates/spacewasm_c_api/cbindgen.toml b/crates/spacewasm_c_api/cbindgen.toml index a6d32a0..075be3d 100644 --- a/crates/spacewasm_c_api/cbindgen.toml +++ b/crates/spacewasm_c_api/cbindgen.toml @@ -11,6 +11,25 @@ header = """/* * spacewasm.h — C API for the SpaceWasm WebAssembly interpreter. * Generated by cbindgen */""" +after_includes = """ + +#if defined(__has_c_attribute) +# if __has_c_attribute(noreturn) +# define SPACEWASM_NORETURN [[noreturn]] /* Standard C23 attribute */ +# endif +#endif + +#if !defined(SPACEWASM_NORETURN) +# if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) +# define SPACEWASM_NORETURN _Noreturn /* Standard C11 keyword */ +# elif defined(__GNUC__) || defined(__clang__) +# define SPACEWASM_NORETURN __attribute__((noreturn)) /* GCC / Clang extension */ +# elif defined(_MSC_VER) +# define SPACEWASM_NORETURN __declspec(noreturn) /* MSVC extension */ +# else +# define SPACEWASM_NORETURN /* Fallback: do nothing on unsupported compilers */ +# endif +#endif""" [export] include = ["MemoryStatistics"] diff --git a/crates/spacewasm_c_api/include/spacewasm.h b/crates/spacewasm_c_api/include/spacewasm.h index 0d4da33..33eac85 100644 --- a/crates/spacewasm_c_api/include/spacewasm.h +++ b/crates/spacewasm_c_api/include/spacewasm.h @@ -10,6 +10,26 @@ #include #include +#if defined(__has_c_attribute) +# if __has_c_attribute(noreturn) +# define SPACEWASM_NORETURN [[noreturn]] /* Standard C23 attribute */ +# endif +#endif + +#if !defined(SPACEWASM_NORETURN) +# if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) +# define SPACEWASM_NORETURN _Noreturn /* Standard C11 keyword */ +# elif defined(__GNUC__) || defined(__clang__) +# define SPACEWASM_NORETURN __attribute__((noreturn)) /* GCC / Clang extension */ +# elif defined(_MSC_VER) +# define SPACEWASM_NORETURN __declspec(noreturn) /* MSVC extension */ +# else +# define SPACEWASM_NORETURN /* Fallback: do nothing on unsupported compilers */ +# endif +#endif + +#define CustomSection_MAX_NAME_LENGTH 32 + /* Operation status returned by most `spacewasm_*` functions. [`spacewasm_status_t::SPACEWASM_OK`] (0) means success. @@ -75,7 +95,8 @@ enum spacewasm_status_t SPACEWASM_ERR_LABEL_JUMP_TOO_LARGE = 100, SPACEWASM_ERR_TYPE_MISMATCH = 101, SPACEWASM_ERR_BLOCK_RESULT_TYPE_MISMATCH = 102, - SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 103, + SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH = 103, + SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 104, SPACEWASM_ERR_ILLEGAL_MEMORY_GROW = 112, SPACEWASM_ERR_INVALID_ELEMENT_OFFSET = 113, SPACEWASM_ERR_INVALID_ELEMENT_OUT_OF_BOUNDS = 114, @@ -104,7 +125,7 @@ enum spacewasm_status_t SPACEWASM_ERR_TABLE_IMPORT_NOT_FOUND = 147, SPACEWASM_ERR_FUNCTION_IMPORT_OUT_OF_RANGE = 148, SPACEWASM_ERR_FUNCTION_IMPORT_TYPE_MISMATCH = 149, - SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE = 150, + SPACEWASM_ERR_GLOBAL_NOT_MUTABLE = 150, SPACEWASM_ERR_GLOBAL_IMPORT_TYPE_MISMATCH = 151, SPACEWASM_ERR_MEMORY_IMPORT_TYPE_MISMATCH = 152, SPACEWASM_ERR_TABLE_IMPORT_TYPE_MISMATCH = 153, @@ -443,23 +464,12 @@ typedef void (*spacewasm_global_dealloc_fn_t)(void *userdata, size_t size, size_t align); -typedef struct spacewasm_memory_statistics_t { - int32_t total_bytes; - int32_t pad_bytes; -} spacewasm_memory_statistics_t; - #ifdef __cplusplus extern "C" { #endif // __cplusplus -/* - Global allocator statistics. Independent of the interpreter configuration, - so it takes no const-generic parameters. - */ -spacewasm_memory_statistics_t spacewasm_memory_statistics(void); - /* Create a guest linear-memory allocator from three C callbacks, returning an opaque handle (or null if any callback is null or allocation fails). The @@ -767,6 +777,12 @@ spacewasm_status_t spacewasm_reset(struct spacewasm_t *engine); Fetch the result of the last completed call, coerced to `expected`, into `out`. + # Silent type coercion + + The core engine stores a completed call's result as an untagged + [`RawValue`]. The function signature must be checked before invoking and the return + value must be extracted (in this function) using the proper `expected` type. + # Safety `engine` must be live; `out` valid. */ @@ -828,7 +844,7 @@ spacewasm_status_t spacewasm_mem_size(struct spacewasm_caller_t *caller, uint32_ returns: ! */ -extern void spacewasm_panic(const uint8_t *filename, +SPACEWASM_NORETURN extern void spacewasm_panic(const uint8_t *filename, size_t filename_len, uint32_t line, const uint8_t *msg, @@ -842,9 +858,9 @@ extern void spacewasm_panic(const uint8_t *filename, `alloc`/`dealloc` must remain valid for the lifetime of the process and honor the requested size/alignment. `userdata` must outlive all allocations. */ -int32_t spacewasm_set_global_allocator(spacewasm_global_alloc_fn_t alloc, - spacewasm_global_dealloc_fn_t dealloc, - void *userdata); +spacewasm_status_t spacewasm_set_global_allocator(spacewasm_global_alloc_fn_t alloc, + spacewasm_global_dealloc_fn_t dealloc, + void *userdata); #ifdef __cplusplus } // extern "C" diff --git a/crates/spacewasm_std/src/main.rs b/crates/spacewasm_std/src/main.rs index 171c7a3..a1519a2 100644 --- a/crates/spacewasm_std/src/main.rs +++ b/crates/spacewasm_std/src/main.rs @@ -1,7 +1,7 @@ use spacewasm::{ CodeBuilder, CompilerOptions, ExportDesc, HostFunction, HostFunctionBreak, HostModule, - InterpreterResult, InterpreterRunner, ModuleRef, PageAllocator, Ref, SectionKind, ValType, - Value, WasmRef, vec, + InterpreterResult, InterpreterRunner, ModuleRef, PageAllocator, Ref, ValType, Value, WasmRef, + vec, }; use spacewasm_util::{FileStream, RustSystemAllocator}; use std::ops::ControlFlow; @@ -148,19 +148,18 @@ fn main() { let file = std::fs::File::open(path).expect("failed to open file"); let mut file_stream = FileStream::new(file); - let (module, stats) = - match spacewasm::Module::new_with_statistics::( - "main", - &mut file_stream, - &mut state.store, - &mut code_builder, - spacewasm::Rc::new(RustSystemAllocator) - .unwrap() - .into_wasm_memory_allocator(), - ) { - Ok(parsed) => parsed, - Err(e) => guest_error(format!("failed to decode/validate wasm module: {e:?}")), - }; + let module = match spacewasm::Module::new::( + "main", + &mut file_stream, + &mut state.store, + &mut code_builder, + spacewasm::Rc::new(RustSystemAllocator) + .unwrap() + .into_wasm_memory_allocator(), + ) { + Ok(parsed) => parsed, + Err(e) => guest_error(format!("failed to decode/validate wasm module: {e:?}")), + }; let text = code_builder.pages(); let final_page_offset = code_builder.offset(); @@ -183,19 +182,15 @@ fn main() { let module = state.store.modules().last().unwrap(); - let mut total: usize = 0; - for (i, section) in stats.iter().enumerate() { - let section_kind = SectionKind::convert(i as u8).unwrap(); - eprintln!("{:?}: {} bytes", section_kind, section.total_bytes); - total += section.total_bytes as usize; - } - + let stats = unsafe { GLOBAL_ALLOCATOR.read() }.stats(); let wasm_size = file_stream.len(); - eprintln!("Total: {}", total); + eprintln!("Total: {}", stats.total_bytes); + eprintln!("Pad bytes: {}", stats.pad_bytes); + eprintln!("Pages: {}", stats.pages); eprintln!( "Compilation Ratio: {:.2}x", - (total as f64) / (wasm_size as f64) + (stats.total_bytes as f64) / (wasm_size as f64) ); let full_page_usage = if text.len() > 1 { From 010b19da04dd44b3bb93f3d2ba5dfc02a76baf65 Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Thu, 3 Sep 2026 07:47:28 -0700 Subject: [PATCH 4/7] Fix memory statistics after removal --- crates/spacewasm_c_api/cbindgen.toml | 4 - .../examples/ctest_suite.c | 114 ++++++------------ src/util/alloc.rs | 4 +- src/util/box_.rs | 4 - src/util/vec.rs | 2 - 5 files changed, 42 insertions(+), 86 deletions(-) diff --git a/crates/spacewasm_c_api/cbindgen.toml b/crates/spacewasm_c_api/cbindgen.toml index 075be3d..165a7bc 100644 --- a/crates/spacewasm_c_api/cbindgen.toml +++ b/crates/spacewasm_c_api/cbindgen.toml @@ -32,14 +32,12 @@ after_includes = """ #endif""" [export] -include = ["MemoryStatistics"] # `__spacewasm_*` are the heap-allocator symbols the core crate imports; they are # provided internally, not part of the public C surface, so keep them out of the header. exclude = [ "JumpTarget", "__spacewasm_alloc", "__spacewasm_dealloc", - "__spacewasm_memory_statistics", "rust_eh_personality", ] @@ -48,9 +46,7 @@ exclude = [ "SpacewasmCaller" = "spacewasm_caller_t" "CEngine" = "spacewasm_t" "HostModule" = "spacewasm_host_module_t" -"MemoryStatistics" = "spacewasm_memory_statistics_t" [parse] -# Follow the re-export of `MemoryStatistics` into the core crate. parse_deps = true include = ["spacewasm"] diff --git a/crates/spacewasm_c_example/examples/ctest_suite.c b/crates/spacewasm_c_example/examples/ctest_suite.c index f180b8f..ab7e70d 100644 --- a/crates/spacewasm_c_example/examples/ctest_suite.c +++ b/crates/spacewasm_c_example/examples/ctest_suite.c @@ -98,22 +98,19 @@ static const uint8_t HOST_WASM[] = { /* (module (import "env" "pause") (func (export "test_pause") (result i32) * (call 0) (i32.const 42))) */ static const uint8_t PAUSE_WASM[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, - 0x00, 0x00, 0x60, 0x00, 0x01, 0x7f, 0x02, 0x0d, 0x01, 0x03, 0x65, 0x6e, - 0x76, 0x05, 0x70, 0x61, 0x75, 0x73, 0x65, 0x00, 0x00, 0x03, 0x02, 0x01, - 0x01, 0x07, 0x0e, 0x01, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, - 0x75, 0x73, 0x65, 0x00, 0x01, 0x0a, 0x08, 0x01, 0x06, 0x00, 0x10, 0x00, - 0x41, 0x2a, 0x0b}; + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, 0x00, 0x00, 0x60, 0x00, + 0x01, 0x7f, 0x02, 0x0d, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x05, 0x70, 0x61, 0x75, 0x73, 0x65, 0x00, + 0x00, 0x03, 0x02, 0x01, 0x01, 0x07, 0x0e, 0x01, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, + 0x75, 0x73, 0x65, 0x00, 0x01, 0x0a, 0x08, 0x01, 0x06, 0x00, 0x10, 0x00, 0x41, 0x2a, 0x0b}; /* (module (import "env" "pause_i32") (func (export "test_pause_i32") (result i32) * (call 0))) */ static const uint8_t PAUSE_I32_WASM[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, - 0x00, 0x01, 0x7f, 0x02, 0x11, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x09, 0x70, - 0x61, 0x75, 0x73, 0x65, 0x5f, 0x69, 0x33, 0x32, 0x00, 0x00, 0x03, 0x02, - 0x01, 0x00, 0x07, 0x12, 0x01, 0x0e, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, - 0x61, 0x75, 0x73, 0x65, 0x5f, 0x69, 0x33, 0x32, 0x00, 0x01, 0x0a, 0x06, - 0x01, 0x04, 0x00, 0x10, 0x00, 0x0b}; + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, + 0x7f, 0x02, 0x11, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x09, 0x70, 0x61, 0x75, 0x73, 0x65, + 0x5f, 0x69, 0x33, 0x32, 0x00, 0x00, 0x03, 0x02, 0x01, 0x00, 0x07, 0x12, 0x01, 0x0e, + 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x75, 0x73, 0x65, 0x5f, 0x69, 0x33, 0x32, + 0x00, 0x01, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x10, 0x00, 0x0b}; /* (module * (global $g (export "g") (mut i32) (i32.const 7)) @@ -121,13 +118,11 @@ static const uint8_t PAUSE_I32_WASM[] = { * (func (export "get_g") (result i32) global.get $g) * (func (export "set_g") (param i32) local.get 0 global.set $g)) */ static const uint8_t GLOBALS_WASM[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x02, 0x60, - 0x00, 0x01, 0x7f, 0x60, 0x01, 0x7f, 0x00, 0x03, 0x03, 0x02, 0x00, 0x01, - 0x06, 0x0b, 0x02, 0x7f, 0x01, 0x41, 0x07, 0x0b, 0x7f, 0x00, 0x41, 0x2a, - 0x0b, 0x07, 0x19, 0x04, 0x01, 0x67, 0x03, 0x00, 0x01, 0x63, 0x03, 0x01, - 0x05, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x00, 0x00, 0x05, 0x73, 0x65, 0x74, - 0x5f, 0x67, 0x00, 0x01, 0x0a, 0x0d, 0x02, 0x04, 0x00, 0x23, 0x00, 0x0b, - 0x06, 0x00, 0x20, 0x00, 0x24, 0x00, 0x0b}; + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x02, 0x60, 0x00, 0x01, 0x7f, 0x60, + 0x01, 0x7f, 0x00, 0x03, 0x03, 0x02, 0x00, 0x01, 0x06, 0x0b, 0x02, 0x7f, 0x01, 0x41, 0x07, 0x0b, + 0x7f, 0x00, 0x41, 0x2a, 0x0b, 0x07, 0x19, 0x04, 0x01, 0x67, 0x03, 0x00, 0x01, 0x63, 0x03, 0x01, + 0x05, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x00, 0x00, 0x05, 0x73, 0x65, 0x74, 0x5f, 0x67, 0x00, 0x01, + 0x0a, 0x0d, 0x02, 0x04, 0x00, 0x23, 0x00, 0x0b, 0x06, 0x00, 0x20, 0x00, 0x24, 0x00, 0x0b}; /* (module * (global $gi (export "gi") (mut i32) (i32.const 10)) @@ -136,20 +131,18 @@ static const uint8_t GLOBALS_WASM[] = { * (global $gd (export "gd") (mut f64) (f64.const 2.5)) * (func (export "get_gI") (result i64) global.get $gI)) */ static const uint8_t GLOBALS_MULTI_WASM[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, - 0x00, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x06, 0x1f, 0x04, 0x7f, 0x01, - 0x41, 0x0a, 0x0b, 0x7e, 0x01, 0x42, 0x14, 0x0b, 0x7d, 0x01, 0x43, 0x00, - 0x00, 0xc0, 0x3f, 0x0b, 0x7c, 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, 0x40, 0x0b, 0x07, 0x1e, 0x05, 0x02, 0x67, 0x69, 0x03, 0x00, - 0x02, 0x67, 0x49, 0x03, 0x01, 0x02, 0x67, 0x66, 0x03, 0x02, 0x02, 0x67, - 0x64, 0x03, 0x03, 0x06, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x49, 0x00, 0x00, - 0x0a, 0x06, 0x01, 0x04, 0x00, 0x23, 0x01, 0x0b}; + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7e, 0x03, + 0x02, 0x01, 0x00, 0x06, 0x1f, 0x04, 0x7f, 0x01, 0x41, 0x0a, 0x0b, 0x7e, 0x01, 0x42, 0x14, 0x0b, + 0x7d, 0x01, 0x43, 0x00, 0x00, 0xc0, 0x3f, 0x0b, 0x7c, 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x40, 0x0b, 0x07, 0x1e, 0x05, 0x02, 0x67, 0x69, 0x03, 0x00, 0x02, 0x67, 0x49, 0x03, + 0x01, 0x02, 0x67, 0x66, 0x03, 0x02, 0x02, 0x67, 0x64, 0x03, 0x03, 0x06, 0x67, 0x65, 0x74, 0x5f, + 0x67, 0x49, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x23, 0x01, 0x0b}; /* Exporter half of the imported-global pair, module "b": * (module (global (export "bg") (mut i32) (i32.const 55))) */ -static const uint8_t GLOBAL_EXPORTER_WASM[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x06, 0x06, 0x01, 0x7f, - 0x01, 0x41, 0x37, 0x0b, 0x07, 0x06, 0x01, 0x02, 0x62, 0x67, 0x03, 0x00}; +static const uint8_t GLOBAL_EXPORTER_WASM[] = {0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x06, 0x06, 0x01, 0x7f, 0x01, 0x41, 0x37, 0x0b, + 0x07, 0x06, 0x01, 0x02, 0x62, 0x67, 0x03, 0x00}; /* Importer half, module "a": imports b.bg (index-space global 0), then defines * and exports its own mutable i32 `ag` (module-local global 0, index-space 1), @@ -160,10 +153,9 @@ static const uint8_t GLOBAL_EXPORTER_WASM[] = { * (global $ag (export "ag") (mut i32) (i32.const 11)) * (export "reexport" (global $ig))) */ static const uint8_t GLOBAL_IMPORTER_WASM[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x02, 0x09, 0x01, 0x01, - 0x62, 0x02, 0x62, 0x67, 0x03, 0x7f, 0x01, 0x06, 0x06, 0x01, 0x7f, 0x01, - 0x41, 0x0b, 0x0b, 0x07, 0x11, 0x02, 0x02, 0x61, 0x67, 0x03, 0x01, 0x08, - 0x72, 0x65, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x03, 0x00}; + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x02, 0x09, 0x01, 0x01, 0x62, 0x02, 0x62, 0x67, + 0x03, 0x7f, 0x01, 0x06, 0x06, 0x01, 0x7f, 0x01, 0x41, 0x0b, 0x0b, 0x07, 0x11, 0x02, 0x02, 0x61, + 0x67, 0x03, 0x01, 0x08, 0x72, 0x65, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x03, 0x00}; /* ---- helpers ------------------------------------------------------------- */ @@ -263,8 +255,7 @@ static spacewasm_status_t load_module_onto(spacewasm_allocator_t* alloc, spacewa return rs == SPACEWASM_RUN_FINISHED ? SPACEWASM_OK : SPACEWASM_ERR_WRONG_STATE; } -static spacewasm_run_status_t run_to_completion(spacewasm_t* store, - spacewasm_trap_t* out_trap) { +static spacewasm_run_status_t run_to_completion(spacewasm_t* store, spacewasm_trap_t* out_trap) { spacewasm_run_status_t rs = SPACEWASM_RUN_OUT_OF_FUEL; while (rs == SPACEWASM_RUN_OUT_OF_FUEL) { rs = spacewasm_run(store, 1000, out_trap); @@ -488,7 +479,8 @@ static int test_globals(void) { CHECK(out.u.i32_ == 100, "g after set = %d", out.u.i32_); uint32_t get_g = 0; - CHECK(spacewasm_find_export_func(store, mod_idx, "get_g", &get_g) == SPACEWASM_OK, "find get_g"); + CHECK(spacewasm_find_export_func(store, mod_idx, "get_g", &get_g) == SPACEWASM_OK, + "find get_g"); CHECK(spacewasm_invoke(store, mod_idx, get_g, NULL, 0) == SPACEWASM_OK, "invoke get_g"); spacewasm_trap_t trap = SPACEWASM_TRAP_NONE; CHECK(run_to_completion(store, &trap) == SPACEWASM_RUN_FINISHED, "run get_g"); @@ -497,7 +489,8 @@ static int test_globals(void) { /* Conversely, `set_g` from Wasm is observable through get_global. */ uint32_t set_g = 0; - CHECK(spacewasm_find_export_func(store, mod_idx, "set_g", &set_g) == SPACEWASM_OK, "find set_g"); + CHECK(spacewasm_find_export_func(store, mod_idx, "set_g", &set_g) == SPACEWASM_OK, + "find set_g"); spacewasm_value_t arg = i32_val(5); CHECK(spacewasm_invoke(store, mod_idx, set_g, &arg, 1) == SPACEWASM_OK, "invoke set_g"); CHECK(run_to_completion(store, &trap) == SPACEWASM_RUN_FINISHED, "run set_g"); @@ -736,22 +729,12 @@ static int test_null_arg_handling(void) { /* NULL store to find_export_func. */ uint32_t idx = 0; - CHECK(spacewasm_find_export_func(NULL, 0, "add", &idx) == SPACEWASM_ERR_NULL_ARG, - "null store"); + CHECK(spacewasm_find_export_func(NULL, 0, "add", &idx) == SPACEWASM_ERR_NULL_ARG, "null store"); spacewasm_destroy(store); return 0; } -static int test_statistics_available(void) { - spacewasm_memory_statistics_t stats = spacewasm_memory_statistics(); - /* Reported by the page allocator's local tracking; just confirm it's wired. - */ - (void)stats.total_bytes; - (void)stats.pad_bytes; - return 0; -} - static int run_add_once(void) { spacewasm_host_t host; if (spacewasm_host_new(0, &host) != SPACEWASM_OK) { @@ -781,19 +764,6 @@ static int run_add_once(void) { return rc; } -/* Create and destroy many stores; the tracked live-byte total must return to - * its baseline, validating drop order and that names/closures are freed. */ -static int test_no_leak_across_lifecycle(void) { - CHECK(run_add_once() == 0, "warmup"); /* absorb one-time allocations */ - int32_t baseline = spacewasm_memory_statistics().total_bytes; - for (int i = 0; i < 50; i++) { - CHECK(run_add_once() == 0, "iter %d", i); - } - int32_t after = spacewasm_memory_statistics().total_bytes; - CHECK(after == baseline, "memory drifted: baseline=%d after=%d", baseline, after); - return 0; -} - /* ---- pause/resume host callbacks ----------------------------------------- */ static spacewasm_hostcall_result_t pause_host(spacewasm_caller_t* caller, void* userdata, @@ -825,8 +795,7 @@ static int test_pause_and_resume_no_value(void) { CHECK(spacewasm_host_new(1, &host) == SPACEWASM_OK, "host_new"); uint32_t hmod; - CHECK(spacewasm_add_host_module(&host, "env", 1, 0, &hmod) == SPACEWASM_OK, - "add_host_module"); + CHECK(spacewasm_add_host_module(&host, "env", 1, 0, &hmod) == SPACEWASM_OK, "add_host_module"); CHECK(spacewasm_add_host_function(&host, hmod, "pause", "", "", pause_host, NULL) == SPACEWASM_OK, "add_host_function"); @@ -836,11 +805,11 @@ static int test_pause_and_resume_no_value(void) { CHECK(spacewasm_new(&host, 1024, 1, opts, &store) == SPACEWASM_OK, "store_new"); cursor_t cursor = {PAUSE_WASM, sizeof(PAUSE_WASM), 0, 0}; - spacewasm_allocator_t* alloc = spacewasm_allocator_new(mem_alloc, mem_realloc, mem_dealloc, NULL); + spacewasm_allocator_t* alloc = + spacewasm_allocator_new(mem_alloc, mem_realloc, mem_dealloc, NULL); CHECK(alloc != NULL, "allocator_new"); uint32_t idx; - CHECK(spacewasm_load_module(store, "main", cursor_read, &cursor, alloc, &idx) == - SPACEWASM_OK, + CHECK(spacewasm_load_module(store, "main", cursor_read, &cursor, alloc, &idx) == SPACEWASM_OK, "load_module"); /* No start function */ @@ -882,8 +851,7 @@ static int test_pause_and_resume_with_value(void) { CHECK(spacewasm_host_new(1, &host) == SPACEWASM_OK, "host_new"); uint32_t hmod; - CHECK(spacewasm_add_host_module(&host, "env", 1, 0, &hmod) == SPACEWASM_OK, - "add_host_module"); + CHECK(spacewasm_add_host_module(&host, "env", 1, 0, &hmod) == SPACEWASM_OK, "add_host_module"); CHECK(spacewasm_add_host_function(&host, hmod, "pause_i32", "", "i", pause_i32_host, NULL) == SPACEWASM_OK, "add_host_function"); @@ -893,11 +861,11 @@ static int test_pause_and_resume_with_value(void) { CHECK(spacewasm_new(&host, 1024, 1, opts, &store) == SPACEWASM_OK, "store_new"); cursor_t cursor = {PAUSE_I32_WASM, sizeof(PAUSE_I32_WASM), 0, 0}; - spacewasm_allocator_t* alloc = spacewasm_allocator_new(mem_alloc, mem_realloc, mem_dealloc, NULL); + spacewasm_allocator_t* alloc = + spacewasm_allocator_new(mem_alloc, mem_realloc, mem_dealloc, NULL); CHECK(alloc != NULL, "allocator_new"); uint32_t idx; - CHECK(spacewasm_load_module(store, "main", cursor_read, &cursor, alloc, &idx) == - SPACEWASM_OK, + CHECK(spacewasm_load_module(store, "main", cursor_read, &cursor, alloc, &idx) == SPACEWASM_OK, "load_module"); /* No start function */ @@ -957,10 +925,8 @@ int main(void) { {"globals_imported", test_globals_imported}, {"error_paths", test_error_paths}, {"null_arg_handling", test_null_arg_handling}, - {"statistics_available", test_statistics_available}, {"pause_and_resume_no_value", test_pause_and_resume_no_value}, {"pause_and_resume_with_value", test_pause_and_resume_with_value}, - {"no_leak_across_lifecycle", test_no_leak_across_lifecycle}, }; int failures = 0; diff --git a/src/util/alloc.rs b/src/util/alloc.rs index 55dc251..c94e57b 100644 --- a/src/util/alloc.rs +++ b/src/util/alloc.rs @@ -42,8 +42,8 @@ unsafe extern "C" { fn __spacewasm_dealloc(ptr: *mut u8, size: usize, align: usize); } -/// Installs the process-global allocator that backs the `__spacewasm_alloc` / -/// `__spacewasm_dealloc` / `__spacewasm_memory_statistics` FFI symbols. +/// Installs the process-global allocator that backs the `__spacewasm_alloc` and +/// `__spacewasm_dealloc` FFI symbols. /// /// # Single-threaded requirement (UB otherwise) /// diff --git a/src/util/box_.rs b/src/util/box_.rs index b4ccce4..6637c32 100644 --- a/src/util/box_.rs +++ b/src/util/box_.rs @@ -391,10 +391,6 @@ mod kani_proofs { unsafe { self.inner.dealloc(ptr, layout) } } - - fn memory_statistics(&self) -> crate::MemoryStatistics { - self.inner.memory_statistics() - } } let backing = RustSystemAllocator; diff --git a/src/util/vec.rs b/src/util/vec.rs index 54c8e53..1882570 100644 --- a/src/util/vec.rs +++ b/src/util/vec.rs @@ -221,7 +221,6 @@ impl Vec { /// # Ok(unsafe { std::alloc::alloc(l) }) /// # } /// # unsafe fn dealloc(&self, p: *mut u8, l: core::alloc::Layout) { unsafe { std::alloc::dealloc(p, l) } } - /// # fn memory_statistics(&self) -> spacewasm::MemoryStatistics { spacewasm::MemoryStatistics { total_bytes: 0, pad_bytes: 0 } } /// # } /// # spacewasm::global_allocator!(DocAlloc, DocAlloc); /// use spacewasm::Vec; @@ -259,7 +258,6 @@ impl Vec { /// # Ok(unsafe { std::alloc::alloc(l) }) /// # } /// # unsafe fn dealloc(&self, p: *mut u8, l: core::alloc::Layout) { unsafe { std::alloc::dealloc(p, l) } } - /// # fn memory_statistics(&self) -> spacewasm::MemoryStatistics { spacewasm::MemoryStatistics { total_bytes: 0, pad_bytes: 0 } } /// # } /// # spacewasm::global_allocator!(DocAlloc, DocAlloc); /// use spacewasm::Vec; From bb8b91e46c366213bc1e25efdba28c319a118b61 Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Thu, 3 Sep 2026 07:48:07 -0700 Subject: [PATCH 5/7] Clippy --- tests/util/spectest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/spectest.rs b/tests/util/spectest.rs index 85d3e97..dfb1467 100644 --- a/tests/util/spectest.rs +++ b/tests/util/spectest.rs @@ -1138,7 +1138,7 @@ fn wast2json(source_wast_path: &Path, out_dir: &Path, test_filename: &str) { // Run wast2json to generate Wasm modules and JSON descriptor let output = ProcessCommand::new(&wast2json_bin) - .arg(&source_wast_path) + .arg(source_wast_path) .arg("--enable-custom-page-sizes") .arg("-o") .arg(out_dir.join(format!("{}.json", test_filename))) From 039a27a7df414f4d6ab771b5b18d134e65af353f Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Thu, 3 Sep 2026 08:26:36 -0700 Subject: [PATCH 6/7] Comment cleanup --- src/util/rc.rs | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/util/rc.rs b/src/util/rc.rs index 1e418fd..c50b288 100644 --- a/src/util/rc.rs +++ b/src/util/rc.rs @@ -200,28 +200,8 @@ impl Rc { fat: core::mem::ManuallyDrop, } - // SOUNDNESS / ABI ASSUMPTIONS - // - // The union below reinterprets the *representation* of a raw pointer. - // Rust does not give a *stable* guarantee for pointer layout, but every - // current Rust target follows the de-facto ABI relied on here: - // - // 1. A pointer to an unsized type (`*const U` where `U` is a trait - // object or slice) is a two-word "fat" pointer laid out as - // `(data_address, metadata)` -- exactly `FatPtr`. - // 2. For a struct `RcInner` whose trailing field is the unsized - // `U`, the pointer metadata of `*const RcInner` is identical to - // the metadata of `*const U` (same vtable / slice length). Building - // a fat pointer from the `RcInner` base address plus `U`'s metadata - // therefore yields a valid `*mut RcInner` for the same - // allocation, which is what this function does. - // - // The `const` block makes assumption (1) a per-instantiation - // compile-time check: if `into_dyn` is ever instantiated with a `U` - // that is not a two-word fat-pointer type (e.g. a `Sized` `U`, whose - // pointer is thin), the reinterpretation would be wrong and compilation - // fails here instead of miscompiling silently. `RcInner` is checked - // as well to pin assumption (2)'s width. + // The `const` block validates memory layout assumptions made across the + // fat pointer conversion boundary const { let word = core::mem::size_of::<*const ()>(); assert!( From 3b3dc5dfc20e92d8098c84e244ec692d45d3288d Mon Sep 17 00:00:00 2001 From: Andrei Tumbar Date: Thu, 3 Sep 2026 09:33:27 -0700 Subject: [PATCH 7/7] Fix up regressions --- .github/scripts/run-tests-with-miri.sh | 21 ++++++++++++++++----- .github/workflows/daily.yml | 18 +++++------------- crates/spacewasm_c_api/include/spacewasm.h | 6 +++--- crates/spacewasm_c_api/src/capi.rs | 2 +- crates/spacewasm_c_api/src/status.rs | 4 ++-- src/lib.rs | 17 ++++++++++++----- src/memory.rs | 15 ++++++++++++++- src/util/paging.rs | 20 ++++++++++---------- 8 files changed, 63 insertions(+), 40 deletions(-) diff --git a/.github/scripts/run-tests-with-miri.sh b/.github/scripts/run-tests-with-miri.sh index 4b8e7a2..451cfbd 100755 --- a/.github/scripts/run-tests-with-miri.sh +++ b/.github/scripts/run-tests-with-miri.sh @@ -26,7 +26,7 @@ target_flag() { export -f target_flag # shellcheck disable=SC2206 -targets=(${MIRI_TEST_TARGETS:-lib core_integration regression_integration custom_page_sizes_integration statistics_integration}) +targets=(${MIRI_TEST_TARGETS:-lib core_integration regression_integration custom_page_sizes_integration}) pairs_file="$(mktemp)" results_file="$(mktemp)" @@ -34,10 +34,21 @@ trap 'rm -f "$pairs_file" "$results_file"' EXIT for label in "${targets[@]}"; do target="$(target_flag "$label")" - RUSTFLAGS="--cfg miri" cargo test --quiet $target -- --list 2>/dev/null | - grep ': test$' | sed 's/: test$//' | - sed "s/^/${label}\t/" \ - >>"$pairs_file" + + if ! listing="$(RUSTFLAGS="--cfg miri" cargo test --quiet $target -- --list 2>&1)"; then + echo "!!! could not enumerate tests for target '${label}'" >&2 + echo "$listing" >&2 + exit 1 + fi + + names="$(printf '%s\n' "$listing" | grep ': test$' | sed 's/: test$//' || true)" + if [ -z "$names" ]; then + echo "!!! target '${label}' enumerated zero tests" >&2 + echo " (stale MIRI_TEST_TARGETS entry, or every test is #[ignore]d?)" >&2 + exit 1 + fi + + printf '%s\n' "$names" | sed "s/^/${label}\t/" >>"$pairs_file" done total=$(wc -l <"$pairs_file") diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 0a1952b..3b4d36e 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -18,26 +18,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c with: - profile: minimal - toolchain: nightly - override: true + toolchain: nightly-2026-08-30 components: miri - name: Install WABT (WebAssembly Binary Toolkit) - run: | - WABT_VERSION=1.0.41 - WABT_PLATFORM="linux-x64" - wget https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz - tar -xzf wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz - sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/ - shell: bash + uses: ./.github/actions/install-wabt - name: Convert wast files for Miri run: cargo test --test miri_wast_convert -- --ignored @@ -47,5 +39,5 @@ jobs: MIRIFLAGS: -Zmiri-disable-isolation MIRI_TEST_TIMEOUT_SECS: 1500 MIRI_TEST_JOBS: 4 - MIRI_TEST_TARGETS: core_integration regression_integration custom_page_sizes_integration statistics_integration + MIRI_TEST_TARGETS: core_integration regression_integration custom_page_sizes_integration run: ./.github/scripts/run-tests-with-miri.sh diff --git a/crates/spacewasm_c_api/include/spacewasm.h b/crates/spacewasm_c_api/include/spacewasm.h index 33eac85..6bfc900 100644 --- a/crates/spacewasm_c_api/include/spacewasm.h +++ b/crates/spacewasm_c_api/include/spacewasm.h @@ -95,8 +95,8 @@ enum spacewasm_status_t SPACEWASM_ERR_LABEL_JUMP_TOO_LARGE = 100, SPACEWASM_ERR_TYPE_MISMATCH = 101, SPACEWASM_ERR_BLOCK_RESULT_TYPE_MISMATCH = 102, - SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH = 103, - SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 104, + SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 103, + SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH = 104, SPACEWASM_ERR_ILLEGAL_MEMORY_GROW = 112, SPACEWASM_ERR_INVALID_ELEMENT_OFFSET = 113, SPACEWASM_ERR_INVALID_ELEMENT_OUT_OF_BOUNDS = 114, @@ -706,7 +706,7 @@ spacewasm_status_t spacewasm_get_global(struct spacewasm_t *engine, `global_index` is out of range, [`spacewasm_status_t::SPACEWASM_ERR_GLOBAL_TYPE_MISMATCH`] when the value type does not match the global, and - [`spacewasm_status_t::SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE`] when the global is + [`spacewasm_status_t::SPACEWASM_ERR_GLOBAL_NOT_MUTABLE`] when the global is declared `const`. # Safety diff --git a/crates/spacewasm_c_api/src/capi.rs b/crates/spacewasm_c_api/src/capi.rs index 0ec41f7..1682ea5 100644 --- a/crates/spacewasm_c_api/src/capi.rs +++ b/crates/spacewasm_c_api/src/capi.rs @@ -675,7 +675,7 @@ pub unsafe extern "C" fn spacewasm_get_global( /// `global_index` is out of range, /// [`spacewasm_status_t::SPACEWASM_ERR_GLOBAL_TYPE_MISMATCH`] when the value type /// does not match the global, and -/// [`spacewasm_status_t::SPACEWASM_ERR_GLOBAL_IS_NOT_MUTABLE`] when the global is +/// [`spacewasm_status_t::SPACEWASM_ERR_GLOBAL_NOT_MUTABLE`] when the global is /// declared `const`. /// /// # Safety diff --git a/crates/spacewasm_c_api/src/status.rs b/crates/spacewasm_c_api/src/status.rs index 2cadb3b..b386af3 100644 --- a/crates/spacewasm_c_api/src/status.rs +++ b/crates/spacewasm_c_api/src/status.rs @@ -79,8 +79,8 @@ pub enum spacewasm_status_t { SPACEWASM_ERR_LABEL_JUMP_TOO_LARGE = 100, SPACEWASM_ERR_TYPE_MISMATCH = 101, SPACEWASM_ERR_BLOCK_RESULT_TYPE_MISMATCH = 102, - SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH = 103, - SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 104, + SPACEWASM_ERR_FUNCTION_RESULT_TYPE_MISMATCH = 103, + SPACEWASM_ERR_BR_TABLE_RESULT_TYPE_MISMATCH = 104, // Parse / validation errors - Memory and table validation SPACEWASM_ERR_ILLEGAL_MEMORY_GROW = 112, diff --git a/src/lib.rs b/src/lib.rs index eebd809..cc4e9b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ pub use engine::*; #[cfg(any(test, kani))] pub mod test_support { use crate::alloc::{AllocError, Allocator}; - use core::sync::atomic::{AtomicI32, Ordering}; + use core::sync::atomic::{AtomicIsize, Ordering}; extern crate std; use std::alloc::Layout; @@ -72,8 +72,15 @@ pub mod test_support { #[derive(Clone, Copy)] pub struct RustSystemAllocator; - // Track allocation statistics - static TOTAL_ALLOCATED: AtomicI32 = AtomicI32::new(0); + /// Live bytes handed out by [`RustSystemAllocator`], as a running total. + static TOTAL_ALLOCATED: AtomicIsize = AtomicIsize::new(0); + + impl RustSystemAllocator { + /// Live bytes currently handed out by this allocator. + pub fn total_allocated(&self) -> isize { + TOTAL_ALLOCATED.load(Ordering::Relaxed) + } + } unsafe impl Allocator for RustSystemAllocator { unsafe fn alloc(&self, layout: Layout) -> Result<*mut u8, AllocError> { @@ -82,7 +89,7 @@ pub mod test_support { } else { let ptr = unsafe { std::alloc::alloc(layout) }; if !ptr.is_null() { - TOTAL_ALLOCATED.fetch_add(layout.size() as i32, Ordering::Relaxed); + TOTAL_ALLOCATED.fetch_add(layout.size() as isize, Ordering::Relaxed); } Ok(ptr) } @@ -96,7 +103,7 @@ pub mod test_support { unsafe { std::alloc::dealloc(ptr, layout); } - TOTAL_ALLOCATED.fetch_sub(layout.size() as i32, Ordering::Relaxed); + TOTAL_ALLOCATED.fetch_sub(layout.size() as isize, Ordering::Relaxed); } } } diff --git a/src/memory.rs b/src/memory.rs index aec947e..348ea92 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -46,7 +46,16 @@ impl Rc { /// `Memory` holds a raw pointer, so it is automatically `!Send + !Sync`. /// /// Needs to have inner mutability because multiple modules can point -/// to it (via imports) and we use Rc for reference counting these ownerships. +/// to it (via imports) and we use `Rc` for reference counting these ownerships. +/// +/// # Aliasing model +/// +/// The `store_*` mutators take `&self`, so the borrow checker will not stop you +/// from writing these bytes while a shared reference into them is alive. Drop +/// any [`Memory::load`] / [`Memory::get_slice`] slice before any write — +/// `store_*`, [`Memory::grow`] (which may reallocate), a host callback, or guest +/// re-entry. Holding one across a write is UB under Stacked Borrows. +/// [`Memory::as_shared_cells`] is the one aliasing-tolerant accessor. pub struct Memory { ptr: *mut u8, size: usize, @@ -307,6 +316,10 @@ impl Memory { /// Expose linear memory as `&[UnsafeCell]`. /// + /// Unlike [`get_slice`], this view stays valid across writes, which is what + /// makes it usable as a `wiggle` `GuestMemory::Shared` backing store. Still + /// invalidated by [`Memory::grow`]. Empty for the [`Memory::zero`] sentinel. + /// /// [`get_slice`]: Memory::get_slice pub fn as_shared_cells(&self) -> &[core::cell::UnsafeCell] { if self.ptr.is_null() { diff --git a/src/util/paging.rs b/src/util/paging.rs index 5db9670..c6322a7 100644 --- a/src/util/paging.rs +++ b/src/util/paging.rs @@ -730,11 +730,7 @@ mod kani_proofs { fn proof_drop_safety() { let backing_alloc = RustSystemAllocator; - let initial_stats = backing_alloc.memory_statistics(); - assert_eq!( - initial_stats.total_bytes, 0, - "Backing allocator must start empty" - ); + let initial_bytes = backing_alloc.total_allocated(); { let page_alloc = PageAllocator::<&RustSystemAllocator, 3>::new(&backing_alloc, 128); @@ -752,8 +748,10 @@ mod kani_proofs { assert_eq!(stats.pages, 3, "Must have exactly 3 pages allocated"); // Memory should be allocated - let mid_stats = backing_alloc.memory_statistics(); - assert!(mid_stats.total_bytes > 0, "Memory must be allocated"); + assert!( + backing_alloc.total_allocated() > initial_bytes, + "Memory must be allocated" + ); // Keep pointers alive to prevent early dealloc core::mem::forget((ptr1, ptr2, ptr3)); @@ -762,10 +760,12 @@ mod kani_proofs { // in reverse order: page2, page1, page0 } - // After drop, all memory must be freed - let final_stats = backing_alloc.memory_statistics(); + // After drop, all memory must be freed. Compared against the baseline + // rather than 0: `TOTAL_ALLOCATED` is process-global, so only the delta + // owned by this proof is meaningful. assert_eq!( - final_stats.total_bytes, 0, + backing_alloc.total_allocated(), + initial_bytes, "Drop must free all allocated pages" ); }