From d3132c9dfa247521a6c8e1bcdc762802da37235e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 19 Nov 2024 12:21:36 -0700 Subject: [PATCH] Add a `signals-based-traps` Cargo compile-time feature (#9614) * Gate signal handlers behind a new Cargo feature This commit adds a new on-by-default Cargo feature to the `wasmtime` crate named `signals-based-traps`. This is modeled after the `Config::signals_based_traps` configuration at runtime and can be used to statically disable the use of signal handlers in Wasmtime. This notably reduces the number of platform dependencies that Wasmtime has and provides a mode of avoiding relying on signals altogether. This introduces a new `MallocMemory` which is a linear memory backed by the system allocator. This new type of memory is enabled when virtual memory guards are disabled and signals-based-traps are disabled. This means that this new type of memory will be candidate for fuzzing for example. prtest:full * Fix rebase conflict * Refactor `MmapVec` documentation and representation * Remove no-longer-needed `Arc` * Document it may be backed by `Vec` --- .github/workflows/main.yml | 10 + Cargo.toml | 4 +- crates/cli-flags/Cargo.toml | 1 + crates/cli-flags/src/lib.rs | 54 +-- crates/environ/src/obj.rs | 6 + crates/fuzzing/Cargo.toml | 2 +- crates/fuzzing/src/oracles/memory.rs | 5 +- crates/wasi-nn/Cargo.toml | 2 +- crates/wasi/Cargo.toml | 2 +- crates/wasmtime/Cargo.toml | 39 ++- crates/wasmtime/src/compile/runtime.rs | 6 +- crates/wasmtime/src/config.rs | 27 +- crates/wasmtime/src/engine.rs | 3 + crates/wasmtime/src/lib.rs | 6 + crates/wasmtime/src/runtime/code_memory.rs | 53 ++- crates/wasmtime/src/runtime/module.rs | 4 +- .../wasmtime/src/runtime/module/registry.rs | 1 + crates/wasmtime/src/runtime/store.rs | 17 +- crates/wasmtime/src/runtime/trap.rs | 1 + crates/wasmtime/src/runtime/unix.rs | 7 +- crates/wasmtime/src/runtime/vm.rs | 29 +- crates/wasmtime/src/runtime/vm/cow.rs | 4 +- .../wasmtime/src/runtime/vm/cow_disabled.rs | 46 +++ crates/wasmtime/src/runtime/vm/memory.rs | 320 ++---------------- .../wasmtime/src/runtime/vm/memory/malloc.rs | 97 ++++++ crates/wasmtime/src/runtime/vm/memory/mmap.rs | 214 ++++++++++++ .../vm/{threads => memory}/shared_memory.rs | 2 +- .../shared_memory_disabled.rs | 0 .../wasmtime/src/runtime/vm/memory/static_.rs | 79 +++++ crates/wasmtime/src/runtime/vm/mmap.rs | 5 - crates/wasmtime/src/runtime/vm/mmap_vec.rs | 150 ++++---- .../runtime/vm/{threads => }/parking_spot.rs | 0 .../src/runtime/vm/sys/custom/capi.rs | 17 + .../wasmtime/src/runtime/vm/sys/custom/mod.rs | 6 + .../src/runtime/vm/sys/custom/traphandlers.rs | 9 +- .../wasmtime/src/runtime/vm/sys/custom/vm.rs | 1 + .../src/runtime/vm/sys/miri/traphandlers.rs | 18 +- .../runtime/vm/sys/unix/macos_traphandlers.rs | 39 --- .../wasmtime/src/runtime/vm/sys/unix/mod.rs | 17 +- .../src/runtime/vm/sys/unix/signals.rs | 22 +- .../src/runtime/vm/sys/unix/traphandlers.rs | 74 ++++ .../src/runtime/vm/sys/unix/unwind.rs | 1 + .../runtime/vm/sys/windows/traphandlers.rs | 3 +- crates/wasmtime/src/runtime/vm/threads/mod.rs | 12 - .../wasmtime/src/runtime/vm/traphandlers.rs | 174 +--------- .../src/runtime/vm/traphandlers/signals.rs | 166 +++++++++ crates/wasmtime/src/runtime/windows.rs | 2 + examples/min-platform/Cargo.toml | 3 + examples/min-platform/build.sh | 10 +- examples/min-platform/embedding/Cargo.toml | 3 + examples/min-platform/embedding/cbindgen.toml | 10 + .../min-platform/embedding/src/allocator.rs | 29 +- examples/min-platform/embedding/src/lib.rs | 37 +- .../embedding/wasmtime-platform.c | 8 + .../embedding/wasmtime-platform.h | 35 ++ examples/min-platform/src/main.rs | 11 + src/commands/serve.rs | 34 +- tests/wast.rs | 17 +- 58 files changed, 1239 insertions(+), 715 deletions(-) create mode 100644 crates/wasmtime/src/runtime/vm/cow_disabled.rs create mode 100644 crates/wasmtime/src/runtime/vm/memory/malloc.rs create mode 100644 crates/wasmtime/src/runtime/vm/memory/mmap.rs rename crates/wasmtime/src/runtime/vm/{threads => memory}/shared_memory.rs (99%) rename crates/wasmtime/src/runtime/vm/{threads => memory}/shared_memory_disabled.rs (100%) create mode 100644 crates/wasmtime/src/runtime/vm/memory/static_.rs rename crates/wasmtime/src/runtime/vm/{threads => }/parking_spot.rs (100%) delete mode 100644 crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs create mode 100644 crates/wasmtime/src/runtime/vm/sys/unix/traphandlers.rs delete mode 100644 crates/wasmtime/src/runtime/vm/threads/mod.rs create mode 100644 crates/wasmtime/src/runtime/vm/traphandlers/signals.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16902d03ae1e..b2b8da35e90d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -379,6 +379,8 @@ jobs: -p wasmtime --no-default-features --features threads -p wasmtime --no-default-features --features runtime,threads -p wasmtime --no-default-features --features cranelift,threads + -p wasmtime --no-default-features --features runtime,signals-based-traps + -p wasmtime --no-default-features --features runtime,gc,component-model,signals-based-traps -p wasmtime --features incremental-cache -p wasmtime --all-features @@ -443,7 +445,9 @@ jobs: - uses: ./.github/actions/install-rust - run: rustup target add x86_64-unknown-none + - run: cargo check --target x86_64-unknown-none -p wasmtime --no-default-features --features runtime,component-model - run: cargo check --target x86_64-unknown-none -p wasmtime --no-default-features --features runtime,gc,component-model + - run: cargo check --target x86_64-unknown-none -p wasmtime --no-default-features --features runtime,gc,component-model,signals-based-traps - run: cargo check --target x86_64-unknown-none -p cranelift-control --no-default-features - run: cargo check --target x86_64-unknown-none -p pulley-interpreter --features encode,decode,disas,interp @@ -952,6 +956,12 @@ jobs: # that the regeneration process didn't change anything in-tree. - run: git diff --exit-code + # Test some other feature combinations + - run: ./build.sh x86_64-unknown-none + working-directory: ./examples/min-platform + env: + WASMTIME_SIGNALS_BASED_TRAPS: 1 + # Add the `wasmtime-platform.h` file as a release artifact - uses: actions/upload-artifact@v4 with: diff --git a/Cargo.toml b/Cargo.toml index b479c82c3730..332adb403aca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ rustix = { workspace = true, features = ["mm", "param", "process"] } [dev-dependencies] # depend again on wasmtime to activate its default features for tests -wasmtime = { workspace = true, features = ['default', 'winch', 'pulley', 'all-arch', 'call-hook', 'memory-protection-keys'] } +wasmtime = { workspace = true, features = ['default', 'winch', 'pulley', 'all-arch', 'call-hook', 'memory-protection-keys', 'signals-based-traps'] } env_logger = { workspace = true } log = { workspace = true } filecheck = { workspace = true } @@ -392,6 +392,7 @@ default = [ "addr2line", "debug-builtins", "component-model", + "signals-based-traps", "threads", "gc", "gc-drc", @@ -450,6 +451,7 @@ threads = ["wasmtime-cli-flags/threads"] gc = ["wasmtime-cli-flags/gc", "wasmtime/gc"] gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"] gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"] +signals-based-traps = ["wasmtime/signals-based-traps", "wasmtime-cli-flags/signals-based-traps"] # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help` # for more information on each subcommand. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index 9cf810122915..c1c84ce39dd7 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -35,3 +35,4 @@ gc-drc = ["gc", "wasmtime/gc-drc"] gc-null = ["gc", "wasmtime/gc-null"] threads = ["wasmtime/threads"] memory-protection-keys = ["wasmtime/memory-protection-keys"] +signals-based-traps = ["wasmtime/signals-based-traps"] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index d2e924766e40..4779a1118b90 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -642,39 +642,51 @@ impl CommonOptions { true => err, } - if let Some(max) = self + let memory_reservation = self .opts .memory_reservation - .or(self.opts.static_memory_maximum_size) - { - config.memory_reservation(max); + .or(self.opts.static_memory_maximum_size); + match_feature! { + ["signals-based-traps" : memory_reservation] + size => config.memory_reservation(size), + _ => err, } - if let Some(enable) = self.opts.static_memory_forced { - config.memory_may_move(!enable); + match_feature! { + ["signals-based-traps" : self.opts.static_memory_forced] + enable => config.memory_may_move(!enable), + _ => err, } - if let Some(enable) = self.opts.memory_may_move { - config.memory_may_move(enable); + match_feature! { + ["signals-based-traps" : self.opts.memory_may_move] + enable => config.memory_may_move(enable), + _ => err, } - if let Some(size) = self + let memory_guard_size = self .opts .static_memory_guard_size .or(self.opts.dynamic_memory_guard_size) - .or(self.opts.memory_guard_size) - { - config.memory_guard_size(size); + .or(self.opts.memory_guard_size); + match_feature! { + ["signals-based-traps" : memory_guard_size] + size => config.memory_guard_size(size), + _ => err, } - if let Some(size) = self + let mem_for_growth = self .opts .memory_reservation_for_growth - .or(self.opts.dynamic_memory_reserved_for_growth) - { - config.memory_reservation_for_growth(size); + .or(self.opts.dynamic_memory_reserved_for_growth); + match_feature! { + ["signals-based-traps" : mem_for_growth] + size => config.memory_reservation_for_growth(size), + _ => err, } - if let Some(enable) = self.opts.guard_before_linear_memory { - config.guard_before_linear_memory(enable); + match_feature! { + ["signals-based-traps" : self.opts.guard_before_linear_memory] + enable => config.guard_before_linear_memory(enable), + _ => err, } if let Some(enable) = self.opts.table_lazy_init { config.table_lazy_init(enable); @@ -694,8 +706,10 @@ impl CommonOptions { if let Some(enable) = self.opts.memory_init_cow { config.memory_init_cow(enable); } - if let Some(enable) = self.opts.signals_based_traps { - config.signals_based_traps(enable); + match_feature! { + ["signals-based-traps" : self.opts.signals_based_traps] + enable => config.signals_based_traps(enable), + _ => err, } if let Some(enable) = self.codegen.native_unwind_info { config.native_unwind_info(enable); diff --git a/crates/environ/src/obj.rs b/crates/environ/src/obj.rs index 72ce32fd2e87..a501ddf57fe5 100644 --- a/crates/environ/src/obj.rs +++ b/crates/environ/src/obj.rs @@ -15,6 +15,12 @@ pub const EF_WASMTIME_MODULE: u32 = 1 << 0; /// component. pub const EF_WASMTIME_COMPONENT: u32 = 1 << 1; +/// Flag for the `sh_flags` field in the ELF text section that indicates that +/// the text section does not itself need to be executable. This is used for the +/// Pulley target, for example, to indicate that it does not need to be made +/// natively executable as it does not contain actual native code. +pub const SH_WASMTIME_NOT_EXECUTED: u64 = 1 << 0; + /// A custom Wasmtime-specific section of our compilation image which stores /// mapping data from offsets in the image to offset in the original wasm /// binary. diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml index 66d512a7c596..0e1d86a0bd55 100644 --- a/crates/fuzzing/Cargo.toml +++ b/crates/fuzzing/Cargo.toml @@ -26,7 +26,7 @@ target-lexicon = { workspace = true } tempfile = "3.3.0" wasmparser = { workspace = true } wasmprinter = { workspace = true } -wasmtime = { workspace = true, features = ['default', 'winch', 'gc', 'memory-protection-keys'] } +wasmtime = { workspace = true, features = ['default', 'winch', 'gc', 'memory-protection-keys', 'signals-based-traps'] } wasmtime-wast = { workspace = true, features = ['component-model'] } wasm-encoder = { workspace = true } wasm-smith = { workspace = true } diff --git a/crates/fuzzing/src/oracles/memory.rs b/crates/fuzzing/src/oracles/memory.rs index a3fa0e0f3be7..667139ecd4d5 100644 --- a/crates/fuzzing/src/oracles/memory.rs +++ b/crates/fuzzing/src/oracles/memory.rs @@ -56,7 +56,10 @@ pub fn check_memory_accesses(input: MemoryAccesses) { Ok(x) => x, Err(e) => { log::info!("Failed to instantiate: {e:?}"); - assert!(format!("{e:?}").contains("Cannot allocate memory")); + assert!( + format!("{e:?}").contains("Cannot allocate memory"), + "bad error: {e:?}", + ); return; } }; diff --git a/crates/wasi-nn/Cargo.toml b/crates/wasi-nn/Cargo.toml index 0e8aed8a383f..f4b6c388eeaa 100644 --- a/crates/wasi-nn/Cargo.toml +++ b/crates/wasi-nn/Cargo.toml @@ -59,7 +59,7 @@ cap-std = { workspace = true } libtest-mimic = { workspace = true } test-programs-artifacts = { workspace = true } wasmtime-wasi = { workspace = true, features = ["preview1"] } -wasmtime = { workspace = true, features = ["cranelift"] } +wasmtime = { workspace = true, features = ["cranelift", 'signals-based-traps'] } tracing-subscriber = { workspace = true } [features] diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index db02061ce315..2f903e6a0b4f 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -42,7 +42,7 @@ test-log = { workspace = true } tracing-subscriber = { workspace = true } test-programs-artifacts = { workspace = true } tempfile = { workspace = true } -wasmtime = { workspace = true, features = ['cranelift', 'incremental-cache'] } +wasmtime = { workspace = true, features = ['cranelift', 'incremental-cache', 'signals-based-traps'] } [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["event", "fs", "net"] } diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 5c3ca75a5e6e..5f251ee51ce2 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -137,6 +137,7 @@ default = [ 'component-model', 'threads', 'std', + 'signals-based-traps', ] # An on-by-default feature enabling runtime compilation of WebAssembly modules @@ -188,7 +189,11 @@ async = [ ] # Enables support for the pooling instance allocation strategy -pooling-allocator = ["runtime", "std"] +pooling-allocator = [ + "runtime", + "std", # not ported to no_std yet + "signals-based-traps", # pooling allocation always uses mmap at this time +] # Enables support for all architectures in Cranelift, allowing # cross-compilation using the `wasmtime` crate's API, notably the @@ -236,7 +241,6 @@ runtime = [ "dep:mach2", "dep:memfd", "dep:wasmtime-asm-macros", - "dep:wasmtime-jit-icache-coherence", "dep:wasmtime-slab", "dep:wasmtime-versioned-export-macros", "dep:windows-sys", @@ -263,7 +267,11 @@ runtime = [ # # You can additionally configure which GC implementations are enabled via the # `gc-drc` and `gc-null` features. -gc = ["wasmtime-environ/gc", "wasmtime-cranelift?/gc"] +gc = [ + "wasmtime-environ/gc", + "wasmtime-cranelift?/gc", + "signals-based-traps", # not ported to non-mmap schemes yet +] # Enable the deferred reference counting garbage collector. gc-drc = ["gc", "wasmtime-environ/gc-drc", "wasmtime-cranelift?/gc-drc"] @@ -272,7 +280,11 @@ gc-drc = ["gc", "wasmtime-environ/gc-drc", "wasmtime-cranelift?/gc-drc"] gc-null = ["gc", "wasmtime-environ/gc-null", "wasmtime-cranelift?/gc-null"] # Enable runtime support for the WebAssembly threads proposal. -threads = ["wasmtime-cranelift?/threads", "std"] +threads = [ + "wasmtime-cranelift?/threads", + "std", + "signals-based-traps", +] # Controls whether backtraces will attempt to parse DWARF information in # WebAssembly modules and components to provide filenames and line numbers in @@ -290,6 +302,13 @@ std = [ 'wasmtime-environ/std', 'object/std', 'once_cell', + # technically this isn't necessary but once you have the standard library you + # probably want things to go fast in which case you've probably got signal + # handlers and such so implicitly enable this. This also helps reduce the + # verbosity of others depending on `wasmtime` with `default-features = false` + # where frequently `std` is enabled and this feature will typically want to be + # enabled by default as well. + 'signals-based-traps', ] # Enables support for the `Store::call_hook` API which enables injecting custom @@ -320,3 +339,15 @@ reexport-wasmparser = [] # Enables instances of the traits defined in the wasm-wave crate, which # provides a human-readable text format for component values. wave = ["dep:wasm-wave"] + +# Gates compile-time support for host signals-based-traps. +# +# Traps based on signals, such as SIGSEGV, are useful for accelerating +# WebAssembly by removing explicit checks and letting the hardware deliver +# signals instead. This feature is enabled by default and gates a number +# of implementations within Wasmtime that may rely on virtual memory, for +# example. Embedded systems or smaller systems may wish to disable this feature +# to reduce the runtime requirements of Wasmtime. +signals-based-traps = [ + "dep:wasmtime-jit-icache-coherence", +] diff --git a/crates/wasmtime/src/compile/runtime.rs b/crates/wasmtime/src/compile/runtime.rs index 57ae9c7960e9..781be44829e8 100644 --- a/crates/wasmtime/src/compile/runtime.rs +++ b/crates/wasmtime/src/compile/runtime.rs @@ -159,7 +159,11 @@ impl FinishedObject for MmapVecWrapper { fn write_bytes(&mut self, val: &[u8]) { let mmap = self.mmap.as_mut().expect("write before reserve"); - mmap[self.len..][..val.len()].copy_from_slice(val); + // SAFETY: the `mmap` has not be made readonly yet so it should + // be safe to mutate it. + unsafe { + mmap.as_mut_slice()[self.len..][..val.len()].copy_from_slice(val); + } self.len += val.len(); } } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 108ab48eb1f3..b14a46c58b8e 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -246,7 +246,9 @@ impl Config { async_support: false, module_version: ModuleVersionStrategy::default(), parallel_compilation: !cfg!(miri), - memory_init_cow: true, + // If signals are disabled then virtual memory is probably mostly + // disabled so also disable the use of CoW by default. + memory_init_cow: cfg!(feature = "signals-based-traps"), memory_guaranteed_dense_image_size: 16 << 20, force_memory_init_memfd: false, wmemcheck: false, @@ -1496,6 +1498,7 @@ impl Config { /// /// For 32-bit platforms this value defaults to 10MiB. This means that /// bounds checks will be required on 32-bit platforms. + #[cfg(feature = "signals-based-traps")] pub fn memory_reservation(&mut self, bytes: u64) -> &mut Self { self.tunables.memory_reservation = Some(bytes); self @@ -1531,6 +1534,7 @@ impl Config { /// the memory configuration works at runtime. /// /// The default value for this option is `true`. + #[cfg(feature = "signals-based-traps")] pub fn memory_may_move(&mut self, enable: bool) -> &mut Self { self.tunables.memory_may_move = Some(enable); self @@ -1579,6 +1583,7 @@ impl Config { /// allows eliminating almost all bounds checks on loads/stores with an /// immediate offset of less than 32MiB. On 32-bit platforms this defaults /// to 64KiB. + #[cfg(feature = "signals-based-traps")] pub fn memory_guard_size(&mut self, bytes: u64) -> &mut Self { self.tunables.memory_guard_size = Some(bytes); self @@ -1668,6 +1673,7 @@ impl Config { /// ## Default /// /// This value defaults to `true`. + #[cfg(feature = "signals-based-traps")] pub fn guard_before_linear_memory(&mut self, enable: bool) -> &mut Self { self.tunables.guard_before_linear_memory = Some(enable); self @@ -1783,6 +1789,7 @@ impl Config { /// [`Module::deserialize_file`]: crate::Module::deserialize_file /// [`Module`]: crate::Module /// [IPI]: https://en.wikipedia.org/wiki/Inter-processor_interrupt + #[cfg(feature = "signals-based-traps")] pub fn memory_init_cow(&mut self, enable: bool) -> &mut Self { self.memory_init_cow = enable; self @@ -2036,6 +2043,16 @@ impl Config { None => Tunables::default_host(), }; + // When signals-based traps are disabled use slightly different defaults + // for tunables to be more amenable to `MallocMemory`. Note that these + // can still be overridden by config options. + if !cfg!(feature = "signals-based-traps") { + tunables.signals_based_traps = false; + tunables.memory_reservation = 0; + tunables.memory_guard_size = 0; + tunables.memory_reservation_for_growth = 1 << 20; // 1MB + } + self.tunables.configure(&mut tunables); // If we're going to compile with winch, we must use the winch calling convention. @@ -2060,6 +2077,13 @@ impl Config { None }; + // These `Config` accessors are disabled at compile time so double-check + // the defaults here. + if !cfg!(feature = "signals-based-traps") { + assert!(!tunables.signals_based_traps); + assert!(!self.memory_init_cow); + } + Ok((tunables, features)) } @@ -2394,6 +2418,7 @@ impl Config { /// are enabled by default. /// /// **Note** Disabling this option is not compatible with the Winch compiler. + #[cfg(feature = "signals-based-traps")] pub fn signals_based_traps(&mut self, enable: bool) -> &mut Self { self.tunables.signals_based_traps = Some(enable); self diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index 99f19ae24269..b670ff79eb35 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -97,6 +97,7 @@ impl Engine { // configured. This is the per-program initialization required for // handling traps, such as configuring signals, vectored exception // handlers, etc. + #[cfg(all(feature = "signals-based-traps", not(miri)))] crate::runtime::vm::init_traps(config.macos_use_mach_ports); #[cfg(feature = "debug-builtins")] crate::runtime::vm::debug_builtins::ensure_exported(); @@ -762,10 +763,12 @@ impl Engine { /// If other crashes are seen from using this method please feel free to /// file an issue to update the documentation here with more preconditions /// that must be met. + #[cfg(feature = "signals-based-traps")] pub unsafe fn unload_process_handlers(self) { assert_eq!(Arc::weak_count(&self.inner), 0); assert_eq!(Arc::strong_count(&self.inner), 1); + #[cfg(not(miri))] crate::runtime::vm::deinit_traps(); } } diff --git a/crates/wasmtime/src/lib.rs b/crates/wasmtime/src/lib.rs index 1d07ee3db1fe..0a63124cecb9 100644 --- a/crates/wasmtime/src/lib.rs +++ b/crates/wasmtime/src/lib.rs @@ -266,6 +266,12 @@ //! with the same overhead as the `call-hook` feature where entries/exits into //! WebAssembly will have more overhead than before. //! +//! * `signals-based-traps` - Enabled by default, this enables support for using +//! host signal handlers to implement WebAssembly traps. For example virtual +//! memory is used to catch out-of-bounds accesses in WebAssembly that result +//! in segfaults. This is implicitly enabled by the `std` feature and is the +//! best way to get high-performance WebAssembly. +//! //! More crate features can be found in the [manifest] of Wasmtime itself for //! seeing what can be enabled and disabled. //! diff --git a/crates/wasmtime/src/runtime/code_memory.rs b/crates/wasmtime/src/runtime/code_memory.rs index 5ca7afdaf155..82310248a345 100644 --- a/crates/wasmtime/src/runtime/code_memory.rs +++ b/crates/wasmtime/src/runtime/code_memory.rs @@ -5,9 +5,8 @@ use crate::runtime::vm::{libcalls, MmapVec, UnwindRegistration}; use core::ops::Range; use object::endian::NativeEndian; use object::read::{elf::ElfFile64, Object, ObjectSection}; -use object::ObjectSymbol; +use object::{ObjectSymbol, SectionFlags}; use wasmtime_environ::{lookup_trap_code, obj, Trap}; -use wasmtime_jit_icache_coherence as icache_coherence; /// Management of executable memory within a `MmapVec` /// @@ -20,6 +19,7 @@ pub struct CodeMemory { debug_registration: Option, published: bool, enable_branch_protection: bool, + needs_executable: bool, #[cfg(feature = "debug-builtins")] has_native_debug_info: bool, @@ -65,6 +65,7 @@ impl CodeMemory { let mut text = 0..0; let mut unwind = 0..0; let mut enable_branch_protection = None; + let mut needs_executable = true; #[cfg(feature = "debug-builtins")] let mut has_native_debug_info = false; let mut trap_data = 0..0; @@ -97,6 +98,12 @@ impl CodeMemory { ".text" => { text = range; + if let SectionFlags::Elf { sh_flags } = section.flags() { + if sh_flags & obj::SH_WASMTIME_NOT_EXECUTED != 0 { + needs_executable = false; + } + } + // The text section might have relocations for things like // libcalls which need to be applied, so handle those here. // @@ -141,6 +148,7 @@ impl CodeMemory { published: false, enable_branch_protection: enable_branch_protection .ok_or_else(|| anyhow!("missing `{}` section", obj::ELF_WASM_BTI))?, + needs_executable, #[cfg(feature = "debug-builtins")] has_native_debug_info, text, @@ -253,24 +261,38 @@ impl CodeMemory { // loaded-from-disk images this shouldn't result in IPIs so long as // there weren't any relocations because nothing should have // otherwise written to the image at any point either. + // + // Note that if virtual memory is disabled this is skipped because + // we aren't able to make it readonly, but this is just a + // defense-in-depth measure and isn't required for correctness. + #[cfg(feature = "signals-based-traps")] self.mmap.make_readonly(0..self.mmap.len())?; - let text = self.text(); + // Switch the executable portion from readonly to read/execute. + if self.needs_executable { + #[cfg(feature = "signals-based-traps")] + { + let text = self.text(); - // Clear the newly allocated code from cache if the processor requires it - // - // Do this before marking the memory as R+X, technically we should be able to do it after - // but there are some CPU's that have had errata about doing this with read only memory. - icache_coherence::clear_cache(text.as_ptr().cast(), text.len()) - .expect("Failed cache clear"); + use wasmtime_jit_icache_coherence as icache_coherence; - // Switch the executable portion from readonly to read/execute. - self.mmap - .make_executable(self.text.clone(), self.enable_branch_protection) - .context("unable to make memory executable")?; + // Clear the newly allocated code from cache if the processor requires it + // + // Do this before marking the memory as R+X, technically we should be able to do it after + // but there are some CPU's that have had errata about doing this with read only memory. + icache_coherence::clear_cache(text.as_ptr().cast(), text.len()) + .expect("Failed cache clear"); + + self.mmap + .make_executable(self.text.clone(), self.enable_branch_protection) + .context("unable to make memory executable")?; - // Flush any in-flight instructions from the pipeline - icache_coherence::pipeline_flush_mt().expect("Failed pipeline flush"); + // Flush any in-flight instructions from the pipeline + icache_coherence::pipeline_flush_mt().expect("Failed pipeline flush"); + } + #[cfg(not(feature = "signals-based-traps"))] + bail!("this target requires virtual memory to be enabled"); + } // With all our memory set up use the platform-specific // `UnwindRegistration` implementation to inform the general @@ -309,6 +331,7 @@ impl CodeMemory { obj::LibCall::X86Pshufb => unreachable!(), }; self.mmap + .as_mut_slice() .as_mut_ptr() .add(offset) .cast::() diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs index e381c39c3ce5..6c58b748af48 100644 --- a/crates/wasmtime/src/runtime/module.rs +++ b/crates/wasmtime/src/runtime/module.rs @@ -25,9 +25,7 @@ use wasmtime_environ::{ }; mod registry; -pub use registry::{ - lookup_code, register_code, unregister_code, ModuleRegistry, RegisteredModuleId, -}; +pub use registry::*; /// A compiled WebAssembly module, ready to be instantiated. /// diff --git a/crates/wasmtime/src/runtime/module/registry.rs b/crates/wasmtime/src/runtime/module/registry.rs index 84d0822c31d7..194ff9206aa9 100644 --- a/crates/wasmtime/src/runtime/module/registry.rs +++ b/crates/wasmtime/src/runtime/module/registry.rs @@ -256,6 +256,7 @@ type GlobalRegistry = BTreeMap)>; /// Find which registered region of code contains the given program counter, and /// what offset that PC is within that module's code. +#[cfg(all(feature = "signals-based-traps", not(miri)))] pub fn lookup_code(pc: usize) -> Option<(Arc, usize)> { let all_modules = global_code().read(); let (_end, (start, module)) = all_modules.range(pc..).next()?; diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 7948e7ce2264..810283bd29c4 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -85,7 +85,7 @@ use crate::runtime::vm::mpk::{self, ProtectionKey, ProtectionMask}; use crate::runtime::vm::{ Backtrace, ExportGlobal, GcRootsList, GcStore, InstanceAllocationRequest, InstanceAllocator, InstanceHandle, ModuleRuntimeInfo, OnDemandInstanceAllocator, SignalHandler, StoreBox, - StorePtr, VMContext, VMFuncRef, VMGcRef, VMRuntimeLimits, WasmFault, + StorePtr, VMContext, VMFuncRef, VMGcRef, VMRuntimeLimits, }; use crate::trampoline::VMHostGlobalContext; use crate::type_registry::RegisteredType; @@ -314,7 +314,7 @@ pub struct StoreOpaque { instances: Vec, #[cfg(feature = "component-model")] num_component_instances: usize, - signal_handler: Option>>, + signal_handler: Option, modules: ModuleRegistry, func_refs: FuncRefs, host_globals: Vec>, @@ -1524,7 +1524,7 @@ impl StoreOpaque { } #[cfg_attr(not(target_os = "linux"), allow(dead_code))] // not used on all platforms - pub fn set_signal_handler(&mut self, handler: Option>>) { + pub fn set_signal_handler(&mut self, handler: Option) { self.signal_handler = handler; } @@ -1903,9 +1903,9 @@ impl StoreOpaque { } #[inline] - pub fn signal_handler(&self) -> Option<*const SignalHandler<'static>> { + pub fn signal_handler(&self) -> Option<*const SignalHandler> { let handler = self.signal_handler.as_ref()?; - Some(&**handler as *const _) + Some(handler) } #[inline] @@ -1972,7 +1972,12 @@ impl StoreOpaque { /// with spectre mitigations enabled since the hardware fault address is /// always zero in these situations which means that the trapping context /// doesn't have enough information to report the fault address. - pub(crate) fn wasm_fault(&self, pc: usize, addr: usize) -> Option { + #[cfg(all(feature = "signals-based-traps", not(miri)))] + pub(crate) fn wasm_fault( + &self, + pc: usize, + addr: usize, + ) -> Option { // There are a few instances where a "close to zero" pointer is loaded // and we expect that to happen: // diff --git a/crates/wasmtime/src/runtime/trap.rs b/crates/wasmtime/src/runtime/trap.rs index 649c09152f00..67d3fcb1610f 100644 --- a/crates/wasmtime/src/runtime/trap.rs +++ b/crates/wasmtime/src/runtime/trap.rs @@ -108,6 +108,7 @@ pub(crate) fn from_runtime_box( ); (error, None) } + #[cfg(all(feature = "signals-based-traps", not(miri)))] crate::runtime::vm::TrapReason::Jit { pc, faulting_addr, diff --git a/crates/wasmtime/src/runtime/unix.rs b/crates/wasmtime/src/runtime/unix.rs index 6442502b4a94..5bbcf19cba0b 100644 --- a/crates/wasmtime/src/runtime/unix.rs +++ b/crates/wasmtime/src/runtime/unix.rs @@ -9,14 +9,18 @@ //! throughout the `wasmtime` crate with extra functionality that's only //! available on Unix. +#[cfg(feature = "signals-based-traps")] use crate::prelude::*; -use crate::{AsContextMut, Store}; +#[cfg(feature = "signals-based-traps")] +use crate::AsContextMut; +use crate::Store; /// Extensions for the [`Store`] type only available on Unix. pub trait StoreExt { // TODO: needs more docs? /// The signal handler must be /// [async-signal-safe](http://man7.org/linux/man-pages/man7/signal-safety.7.html). + #[cfg(feature = "signals-based-traps")] unsafe fn set_signal_handler(&mut self, handler: H) where H: 'static @@ -26,6 +30,7 @@ pub trait StoreExt { } impl StoreExt for Store { + #[cfg(feature = "signals-based-traps")] unsafe fn set_signal_handler(&mut self, handler: H) where H: 'static diff --git a/crates/wasmtime/src/runtime/vm.rs b/crates/wasmtime/src/runtime/vm.rs index 48723b21aca3..3728cfd38004 100644 --- a/crates/wasmtime/src/runtime/vm.rs +++ b/crates/wasmtime/src/runtime/vm.rs @@ -29,7 +29,6 @@ mod gc; mod imports; mod instance; mod memory; -mod mmap; mod mmap_vec; mod send_sync_ptr; mod send_sync_unsafe_cell; @@ -39,8 +38,8 @@ mod table; mod traphandlers; mod vmcontext; -mod threads; -pub use self::threads::*; +#[cfg(feature = "threads")] +mod parking_spot; #[cfg(feature = "debug-builtins")] pub mod debug_builtins; @@ -65,8 +64,9 @@ pub use crate::runtime::vm::instance::{ InstanceLimits, PoolConcurrencyLimitError, PoolingInstanceAllocator, PoolingInstanceAllocatorConfig, }; -pub use crate::runtime::vm::memory::{Memory, RuntimeLinearMemory, RuntimeMemoryCreator}; -pub use crate::runtime::vm::mmap::Mmap; +pub use crate::runtime::vm::memory::{ + Memory, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory, +}; pub use crate::runtime::vm::mmap_vec::MmapVec; pub use crate::runtime::vm::mpk::MpkEnabled; pub use crate::runtime::vm::store_box::*; @@ -86,8 +86,21 @@ pub use send_sync_unsafe_cell::SendSyncUnsafeCell; mod module_id; pub use module_id::CompiledModuleId; +#[cfg(feature = "signals-based-traps")] mod cow; -pub use crate::runtime::vm::cow::{MemoryImage, MemoryImageSlot, ModuleMemoryImages}; +#[cfg(not(feature = "signals-based-traps"))] +mod cow_disabled; +#[cfg(feature = "signals-based-traps")] +mod mmap; + +cfg_if::cfg_if! { + if #[cfg(feature = "signals-based-traps")] { + pub use crate::runtime::vm::mmap::Mmap; + pub use self::cow::{MemoryImage, MemoryImageSlot, ModuleMemoryImages}; + } else { + pub use self::cow_disabled::{MemoryImage, MemoryImageSlot, ModuleMemoryImages}; + } +} /// Dynamic runtime functionality needed by this crate throughout the execution /// of a wasm instance. @@ -337,6 +350,7 @@ impl ModuleRuntimeInfo { } /// Returns the host OS page size, in bytes. +#[cfg(feature = "signals-based-traps")] pub fn host_page_size() -> usize { static PAGE_SIZE: AtomicUsize = AtomicUsize::new(0); @@ -352,6 +366,7 @@ pub fn host_page_size() -> usize { } /// Is `bytes` a multiple of the host page size? +#[cfg(feature = "signals-based-traps")] pub fn usize_is_multiple_of_host_page_size(bytes: usize) -> bool { bytes % host_page_size() == 0 } @@ -359,6 +374,7 @@ pub fn usize_is_multiple_of_host_page_size(bytes: usize) -> bool { /// Round the given byte size up to a multiple of the host OS page size. /// /// Returns an error if rounding up overflows. +#[cfg(feature = "signals-based-traps")] pub fn round_u64_up_to_host_pages(bytes: u64) -> Result { let page_size = u64::try_from(crate::runtime::vm::host_page_size()).err2anyhow()?; debug_assert!(page_size.is_power_of_two()); @@ -371,6 +387,7 @@ pub fn round_u64_up_to_host_pages(bytes: u64) -> Result { } /// Same as `round_u64_up_to_host_pages` but for `usize`s. +#[cfg(feature = "signals-based-traps")] pub fn round_usize_up_to_host_pages(bytes: usize) -> Result { let bytes = u64::try_from(bytes).err2anyhow()?; let rounded = round_u64_up_to_host_pages(bytes)?; diff --git a/crates/wasmtime/src/runtime/vm/cow.rs b/crates/wasmtime/src/runtime/vm/cow.rs index 14fc4e1359fc..581716222991 100644 --- a/crates/wasmtime/src/runtime/vm/cow.rs +++ b/crates/wasmtime/src/runtime/vm/cow.rs @@ -107,15 +107,13 @@ impl MemoryImage { assert_page_aligned(start); assert_page_aligned(data_start); assert_page_aligned(data_end); - assert_page_aligned(mmap.original_offset()); #[cfg(feature = "std")] if let Some(file) = mmap.original_file() { if let Some(source) = MemoryImageSource::from_file(file) { return Ok(Some(MemoryImage { source, - source_offset: u64::try_from(mmap.original_offset() + (data_start - start)) - .unwrap(), + source_offset: u64::try_from(data_start - start).unwrap(), linear_memory_offset, len, })); diff --git a/crates/wasmtime/src/runtime/vm/cow_disabled.rs b/crates/wasmtime/src/runtime/vm/cow_disabled.rs new file mode 100644 index 000000000000..8789380deb99 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/cow_disabled.rs @@ -0,0 +1,46 @@ +//! Small shims for CoW support when virtual memory is disabled, meaning that +//! none of the types in this module are supported. + +#![warn(dead_code, unused_imports)] + +use crate::prelude::*; +use crate::runtime::vm::MmapVec; +use alloc::sync::Arc; +use wasmtime_environ::{DefinedMemoryIndex, Module}; + +pub enum ModuleMemoryImages {} + +impl ModuleMemoryImages { + pub fn get_memory_image( + &self, + _defined_index: DefinedMemoryIndex, + ) -> Option<&Arc> { + None + } +} + +#[derive(Debug, PartialEq)] +pub enum MemoryImage {} + +impl ModuleMemoryImages { + pub fn new( + _module: &Module, + _wasm_data: &[u8], + _mmap: Option<&MmapVec>, + ) -> Result> { + Ok(None) + } +} + +#[derive(Debug)] +pub enum MemoryImageSlot {} + +impl MemoryImageSlot { + pub(crate) fn set_heap_limit(&mut self, _size_bytes: usize) -> Result<()> { + match *self {} + } + + pub(crate) fn has_image(&self) -> bool { + match *self {} + } +} diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index 284be6b0c6ed..2b46bdfd25c7 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -75,18 +75,34 @@ //! they should be merged together. use crate::prelude::*; -use crate::runtime::vm::mmap::Mmap; use crate::runtime::vm::vmcontext::VMMemoryDefinition; -use crate::runtime::vm::{ - round_usize_up_to_host_pages, usize_is_multiple_of_host_page_size, MemoryImage, - MemoryImageSlot, SendSyncPtr, SharedMemory, VMStore, WaitResult, -}; +use crate::runtime::vm::{MemoryImage, MemoryImageSlot, VMStore, WaitResult}; use alloc::sync::Arc; use core::ops::Range; -use core::ptr::NonNull; use core::time::Duration; use wasmtime_environ::{Trap, Tunables}; +#[cfg(feature = "signals-based-traps")] +mod mmap; +#[cfg(feature = "signals-based-traps")] +pub use self::mmap::MmapMemory; + +mod malloc; +pub use self::malloc::MallocMemory; + +mod static_; +use self::static_::StaticMemory; + +#[cfg(feature = "threads")] +mod shared_memory; +#[cfg(feature = "threads")] +pub use shared_memory::SharedMemory; + +#[cfg(not(feature = "threads"))] +mod shared_memory_disabled; +#[cfg(not(feature = "threads"))] +pub use shared_memory_disabled::SharedMemory; + /// A memory allocator pub trait RuntimeMemoryCreator: Send + Sync { /// Create new RuntimeLinearMemory @@ -111,7 +127,16 @@ impl RuntimeMemoryCreator for DefaultMemoryCreator { minimum: usize, maximum: Option, ) -> Result> { - Ok(Box::new(MmapMemory::new(ty, tunables, minimum, maximum)?)) + #[cfg(feature = "signals-based-traps")] + if tunables.signals_based_traps + || tunables.memory_guard_size > 0 + || tunables.memory_reservation > 0 + { + return Ok(Box::new(MmapMemory::new(ty, tunables, minimum, maximum)?)); + } + + let _ = maximum; + Ok(Box::new(MallocMemory::new(ty, tunables, minimum)?)) } } @@ -147,284 +172,6 @@ pub trait RuntimeLinearMemory: Send + Sync { } } -/// A linear memory instance. -#[derive(Debug)] -pub struct MmapMemory { - // The underlying allocation. - mmap: Mmap, - - // The current length of this Wasm memory, in bytes. - // - // This region starts at `pre_guard_size` offset from the base of `mmap`. It - // is always accessible, which means that if the Wasm page size is smaller - // than the host page size, there may be some trailing region in the `mmap` - // that is accessible but should not be accessed. (We rely on explicit - // bounds checks in the compiled code to protect this region.) - len: usize, - - // The optional maximum accessible size, in bytes, for this linear memory. - // - // Note that this maximum does not factor in guard pages, so this isn't the - // maximum size of the linear address space reservation for this memory. - // - // This is *not* always a multiple of the host page size, and - // `self.accessible()` may go past `self.maximum` when Wasm is using a small - // custom page size due to `self.accessible()`'s rounding up to the host - // page size. - maximum: Option, - - // The amount of extra bytes to reserve whenever memory grows. This is - // specified so that the cost of repeated growth is amortized. - extra_to_reserve_on_growth: usize, - - // Size in bytes of extra guard pages before the start and after the end to - // optimize loads and stores with constant offsets. - pre_guard_size: usize, - offset_guard_size: usize, -} - -impl MmapMemory { - /// Create a new linear memory instance with specified minimum and maximum - /// number of wasm pages. - pub fn new( - ty: &wasmtime_environ::Memory, - tunables: &Tunables, - minimum: usize, - maximum: Option, - ) -> Result { - // It's a programmer error for these two configuration values to exceed - // the host available address space, so panic if such a configuration is - // found (mostly an issue for hypothetical 32-bit hosts). - // - // Also be sure to round up to the host page size for this value. - let offset_guard_bytes = usize::try_from(tunables.memory_guard_size).unwrap(); - let offset_guard_bytes = round_usize_up_to_host_pages(offset_guard_bytes)?; - let pre_guard_bytes = if tunables.guard_before_linear_memory { - offset_guard_bytes - } else { - 0 - }; - - // Calculate how much is going to be allocated for this linear memory in - // addition to how much extra space we're reserving to grow into. - // - // If the minimum size of this linear memory fits within the initial - // allocation (tunables.memory_reservation) then that's how many bytes - // are going to be allocated. If the maximum size of linear memory - // additionally fits within the entire allocation then there's no need - // to reserve any extra for growth. - // - // If the minimum size doesn't fit within this linear memory. - let mut alloc_bytes = tunables.memory_reservation; - let mut extra_to_reserve_on_growth = tunables.memory_reservation_for_growth; - let minimum_u64 = u64::try_from(minimum).unwrap(); - if minimum_u64 <= alloc_bytes { - if let Ok(max) = ty.maximum_byte_size() { - if max <= alloc_bytes { - extra_to_reserve_on_growth = 0; - } - } - } else { - alloc_bytes = minimum_u64.saturating_add(extra_to_reserve_on_growth); - } - - // Convert `alloc_bytes` and `extra_to_reserve_on_growth` to - // page-aligned `usize` values. - let alloc_bytes = usize::try_from(alloc_bytes).unwrap(); - let extra_to_reserve_on_growth = usize::try_from(extra_to_reserve_on_growth).unwrap(); - let alloc_bytes = round_usize_up_to_host_pages(alloc_bytes)?; - let extra_to_reserve_on_growth = round_usize_up_to_host_pages(extra_to_reserve_on_growth)?; - - let request_bytes = pre_guard_bytes - .checked_add(alloc_bytes) - .and_then(|i| i.checked_add(offset_guard_bytes)) - .ok_or_else(|| format_err!("cannot allocate {} with guard regions", minimum))?; - assert!(usize_is_multiple_of_host_page_size(request_bytes)); - - let mut mmap = Mmap::accessible_reserved(0, request_bytes)?; - - if minimum > 0 { - let accessible = round_usize_up_to_host_pages(minimum)?; - mmap.make_accessible(pre_guard_bytes, accessible)?; - } - - Ok(Self { - mmap, - len: minimum, - maximum, - pre_guard_size: pre_guard_bytes, - offset_guard_size: offset_guard_bytes, - extra_to_reserve_on_growth, - }) - } - - /// Get the length of the accessible portion of the underlying `mmap`. This - /// is the same region as `self.len` but rounded up to a multiple of the - /// host page size. - fn accessible(&self) -> usize { - let accessible = - round_usize_up_to_host_pages(self.len).expect("accessible region always fits in usize"); - debug_assert!(accessible <= self.mmap.len() - self.offset_guard_size - self.pre_guard_size); - accessible - } -} - -impl RuntimeLinearMemory for MmapMemory { - fn byte_size(&self) -> usize { - self.len - } - - fn byte_capacity(&self) -> usize { - self.mmap.len() - self.offset_guard_size - self.pre_guard_size - } - - fn grow_to(&mut self, new_size: usize) -> Result<()> { - assert!(usize_is_multiple_of_host_page_size(self.offset_guard_size)); - assert!(usize_is_multiple_of_host_page_size(self.pre_guard_size)); - assert!(usize_is_multiple_of_host_page_size(self.mmap.len())); - - let new_accessible = round_usize_up_to_host_pages(new_size)?; - if new_accessible > self.mmap.len() - self.offset_guard_size - self.pre_guard_size { - // If the new size of this heap exceeds the current size of the - // allocation we have, then this must be a dynamic heap. Use - // `new_size` to calculate a new size of an allocation, allocate it, - // and then copy over the memory from before. - let request_bytes = self - .pre_guard_size - .checked_add(new_accessible) - .and_then(|s| s.checked_add(self.extra_to_reserve_on_growth)) - .and_then(|s| s.checked_add(self.offset_guard_size)) - .ok_or_else(|| format_err!("overflow calculating size of memory allocation"))?; - assert!(usize_is_multiple_of_host_page_size(request_bytes)); - - let mut new_mmap = Mmap::accessible_reserved(0, request_bytes)?; - new_mmap.make_accessible(self.pre_guard_size, new_accessible)?; - - // This method has an exclusive reference to `self.mmap` and just - // created `new_mmap` so it should be safe to acquire references - // into both of them and copy between them. - unsafe { - let range = self.pre_guard_size..self.pre_guard_size + self.len; - let src = self.mmap.slice(range.clone()); - let dst = new_mmap.slice_mut(range); - dst.copy_from_slice(src); - } - - self.mmap = new_mmap; - } else { - // If the new size of this heap fits within the existing allocation - // then all we need to do is to make the new pages accessible. This - // can happen either for "static" heaps which always hit this case, - // or "dynamic" heaps which have some space reserved after the - // initial allocation to grow into before the heap is moved in - // memory. - assert!(new_size > self.len); - assert!(self.maximum.map_or(true, |max| new_size <= max)); - assert!(new_size <= self.mmap.len() - self.offset_guard_size - self.pre_guard_size); - - let new_accessible = round_usize_up_to_host_pages(new_size)?; - assert!( - new_accessible <= self.mmap.len() - self.offset_guard_size - self.pre_guard_size, - ); - - // If the Wasm memory's page size is smaller than the host's page - // size, then we might not need to actually change permissions, - // since we are forced to round our accessible range up to the - // host's page size. - if new_accessible > self.accessible() { - self.mmap.make_accessible( - self.pre_guard_size + self.accessible(), - new_accessible - self.accessible(), - )?; - } - } - - self.len = new_size; - - Ok(()) - } - - fn set_byte_size(&mut self, len: usize) { - self.len = len; - } - - fn base_ptr(&self) -> *mut u8 { - unsafe { self.mmap.as_mut_ptr().add(self.pre_guard_size) } - } -} - -/// A "static" memory where the lifetime of the backing memory is managed -/// elsewhere. Currently used with the pooling allocator. -struct StaticMemory { - /// The base pointer of this static memory, wrapped up in a send/sync - /// wrapper. - base: SendSyncPtr, - - /// The byte capacity of the `base` pointer. - capacity: usize, - - /// The current size, in bytes, of this memory. - size: usize, -} - -impl StaticMemory { - fn new( - base_ptr: *mut u8, - base_capacity: usize, - initial_size: usize, - maximum_size: Option, - ) -> Result { - if base_capacity < initial_size { - bail!( - "initial memory size of {} exceeds the pooling allocator's \ - configured maximum memory size of {} bytes", - initial_size, - base_capacity, - ); - } - - // Only use the part of the slice that is necessary. - let base_capacity = match maximum_size { - Some(max) if max < base_capacity => max, - _ => base_capacity, - }; - - Ok(Self { - base: SendSyncPtr::new(NonNull::new(base_ptr).unwrap()), - capacity: base_capacity, - size: initial_size, - }) - } -} - -impl RuntimeLinearMemory for StaticMemory { - fn byte_size(&self) -> usize { - self.size - } - - fn byte_capacity(&self) -> usize { - self.capacity - } - - fn grow_to(&mut self, new_byte_size: usize) -> Result<()> { - // Never exceed the static memory size; this check should have been made - // prior to arriving here. - assert!(new_byte_size <= self.capacity); - - // Update our accounting of the available size. - self.size = new_byte_size; - Ok(()) - } - - fn set_byte_size(&mut self, len: usize) { - self.size = len; - } - - fn base_ptr(&self) -> *mut u8 { - self.base.as_ptr() - } -} - /// Representation of a runtime wasm linear memory. pub enum Memory { Local(LocalMemory), @@ -731,6 +478,7 @@ impl LocalMemory { // If a memory image was specified, try to create the MemoryImageSlot on // top of our mmap. let memory_image = match memory_image { + #[cfg(feature = "signals-based-traps")] Some(image) => { let mut slot = MemoryImageSlot::create( alloc.base_ptr().cast(), @@ -744,6 +492,8 @@ impl LocalMemory { slot.instantiate(alloc.byte_size(), Some(image), ty, tunables)?; Some(slot) } + #[cfg(not(feature = "signals-based-traps"))] + Some(_) => unreachable!(), None => None, }; Ok(LocalMemory { diff --git a/crates/wasmtime/src/runtime/vm/memory/malloc.rs b/crates/wasmtime/src/runtime/vm/memory/malloc.rs new file mode 100644 index 000000000000..68551bca0c29 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/memory/malloc.rs @@ -0,0 +1,97 @@ +//! Support for implementing the [`RuntimeLinearMemory`] trait in terms of a +//! platform memory allocation primitive (e.g. `malloc`) +//! +//! Note that memory is allocated here using `Vec::try_reserve` to explicitly +//! handle memory allocation failures. + +use crate::prelude::*; +use crate::runtime::vm::memory::RuntimeLinearMemory; +use crate::runtime::vm::SendSyncPtr; +use core::mem; +use core::ptr::NonNull; +use wasmtime_environ::Tunables; + +#[repr(C, align(16))] +#[derive(Copy, Clone)] +pub struct Align16(u128); + +/// An instance of linear memory backed by the default system allocator. +pub struct MallocMemory { + storage: Vec, + base_ptr: SendSyncPtr, + byte_len: usize, +} + +impl MallocMemory { + pub fn new( + _ty: &wasmtime_environ::Memory, + tunables: &Tunables, + minimum: usize, + ) -> Result { + if tunables.memory_guard_size > 0 { + bail!("malloc memory is only compatible if guard pages aren't used"); + } + if tunables.memory_reservation > 0 { + bail!("malloc memory is only compatible with no ahead-of-time memory reservation"); + } + + let byte_size = minimum + .checked_add( + tunables + .memory_reservation_for_growth + .try_into() + .err2anyhow()?, + ) + .context("memory allocation size too large")?; + + let element_len = byte_size_to_element_len(byte_size); + let mut storage = Vec::new(); + storage.try_reserve(element_len).err2anyhow()?; + storage.resize(element_len, Align16(0)); + Ok(MallocMemory { + base_ptr: SendSyncPtr::new(NonNull::new(storage.as_mut_ptr()).unwrap()).cast(), + storage, + byte_len: minimum, + }) + } +} + +impl RuntimeLinearMemory for MallocMemory { + fn byte_size(&self) -> usize { + self.byte_len + } + + fn byte_capacity(&self) -> usize { + self.storage.capacity() * mem::size_of::() + } + + fn grow_to(&mut self, new_size: usize) -> Result<()> { + let new_element_len = byte_size_to_element_len(new_size); + if new_element_len > self.storage.len() { + self.storage + .try_reserve(new_element_len - self.storage.len()) + .err2anyhow()?; + self.storage.resize(new_element_len, Align16(0)); + self.base_ptr = + SendSyncPtr::new(NonNull::new(self.storage.as_mut_ptr()).unwrap()).cast(); + } + self.byte_len = new_size; + Ok(()) + } + + fn base_ptr(&self) -> *mut u8 { + self.base_ptr.as_ptr() + } +} + +fn byte_size_to_element_len(byte_size: usize) -> usize { + let align = mem::align_of::(); + + // Round up the requested byte size to the size of each vector element. + let byte_size_rounded_up = + byte_size.checked_add(align - 1).unwrap_or(usize::MAX) & !(align - 1); + + // Next divide this aligned size by the size of each element to get the + // element length of our vector. + byte_size_rounded_up / align +} diff --git a/crates/wasmtime/src/runtime/vm/memory/mmap.rs b/crates/wasmtime/src/runtime/vm/memory/mmap.rs new file mode 100644 index 000000000000..0bde35504634 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/memory/mmap.rs @@ -0,0 +1,214 @@ +//! Support for implementing the [`RuntimeLinearMemory`] trait in terms of a +//! platform mmap primitive. + +use crate::prelude::*; +use crate::runtime::vm::memory::RuntimeLinearMemory; +use crate::runtime::vm::mmap::Mmap; +use crate::runtime::vm::{round_usize_up_to_host_pages, usize_is_multiple_of_host_page_size}; +use wasmtime_environ::Tunables; + +/// A linear memory instance. +#[derive(Debug)] +pub struct MmapMemory { + // The underlying allocation. + mmap: Mmap, + + // The current length of this Wasm memory, in bytes. + // + // This region starts at `pre_guard_size` offset from the base of `mmap`. It + // is always accessible, which means that if the Wasm page size is smaller + // than the host page size, there may be some trailing region in the `mmap` + // that is accessible but should not be accessed. (We rely on explicit + // bounds checks in the compiled code to protect this region.) + len: usize, + + // The optional maximum accessible size, in bytes, for this linear memory. + // + // Note that this maximum does not factor in guard pages, so this isn't the + // maximum size of the linear address space reservation for this memory. + // + // This is *not* always a multiple of the host page size, and + // `self.accessible()` may go past `self.maximum` when Wasm is using a small + // custom page size due to `self.accessible()`'s rounding up to the host + // page size. + maximum: Option, + + // The amount of extra bytes to reserve whenever memory grows. This is + // specified so that the cost of repeated growth is amortized. + extra_to_reserve_on_growth: usize, + + // Size in bytes of extra guard pages before the start and after the end to + // optimize loads and stores with constant offsets. + pre_guard_size: usize, + offset_guard_size: usize, +} + +impl MmapMemory { + /// Create a new linear memory instance with specified minimum and maximum + /// number of wasm pages. + pub fn new( + ty: &wasmtime_environ::Memory, + tunables: &Tunables, + minimum: usize, + maximum: Option, + ) -> Result { + // It's a programmer error for these two configuration values to exceed + // the host available address space, so panic if such a configuration is + // found (mostly an issue for hypothetical 32-bit hosts). + // + // Also be sure to round up to the host page size for this value. + let offset_guard_bytes = usize::try_from(tunables.memory_guard_size).unwrap(); + let offset_guard_bytes = round_usize_up_to_host_pages(offset_guard_bytes)?; + let pre_guard_bytes = if tunables.guard_before_linear_memory { + offset_guard_bytes + } else { + 0 + }; + + // Calculate how much is going to be allocated for this linear memory in + // addition to how much extra space we're reserving to grow into. + // + // If the minimum size of this linear memory fits within the initial + // allocation (tunables.memory_reservation) then that's how many bytes + // are going to be allocated. If the maximum size of linear memory + // additionally fits within the entire allocation then there's no need + // to reserve any extra for growth. + // + // If the minimum size doesn't fit within this linear memory. + let mut alloc_bytes = tunables.memory_reservation; + let mut extra_to_reserve_on_growth = tunables.memory_reservation_for_growth; + let minimum_u64 = u64::try_from(minimum).unwrap(); + if minimum_u64 <= alloc_bytes { + if let Ok(max) = ty.maximum_byte_size() { + if max <= alloc_bytes { + extra_to_reserve_on_growth = 0; + } + } + } else { + alloc_bytes = minimum_u64.saturating_add(extra_to_reserve_on_growth); + } + + // Convert `alloc_bytes` and `extra_to_reserve_on_growth` to + // page-aligned `usize` values. + let alloc_bytes = usize::try_from(alloc_bytes).unwrap(); + let extra_to_reserve_on_growth = usize::try_from(extra_to_reserve_on_growth).unwrap(); + let alloc_bytes = round_usize_up_to_host_pages(alloc_bytes)?; + let extra_to_reserve_on_growth = round_usize_up_to_host_pages(extra_to_reserve_on_growth)?; + + let request_bytes = pre_guard_bytes + .checked_add(alloc_bytes) + .and_then(|i| i.checked_add(offset_guard_bytes)) + .ok_or_else(|| format_err!("cannot allocate {} with guard regions", minimum))?; + assert!(usize_is_multiple_of_host_page_size(request_bytes)); + + let mut mmap = Mmap::accessible_reserved(0, request_bytes)?; + + if minimum > 0 { + let accessible = round_usize_up_to_host_pages(minimum)?; + mmap.make_accessible(pre_guard_bytes, accessible)?; + } + + Ok(Self { + mmap, + len: minimum, + maximum, + pre_guard_size: pre_guard_bytes, + offset_guard_size: offset_guard_bytes, + extra_to_reserve_on_growth, + }) + } + + /// Get the length of the accessible portion of the underlying `mmap`. This + /// is the same region as `self.len` but rounded up to a multiple of the + /// host page size. + fn accessible(&self) -> usize { + let accessible = + round_usize_up_to_host_pages(self.len).expect("accessible region always fits in usize"); + debug_assert!(accessible <= self.mmap.len() - self.offset_guard_size - self.pre_guard_size); + accessible + } +} + +impl RuntimeLinearMemory for MmapMemory { + fn byte_size(&self) -> usize { + self.len + } + + fn byte_capacity(&self) -> usize { + self.mmap.len() - self.offset_guard_size - self.pre_guard_size + } + + fn grow_to(&mut self, new_size: usize) -> Result<()> { + assert!(usize_is_multiple_of_host_page_size(self.offset_guard_size)); + assert!(usize_is_multiple_of_host_page_size(self.pre_guard_size)); + assert!(usize_is_multiple_of_host_page_size(self.mmap.len())); + + let new_accessible = round_usize_up_to_host_pages(new_size)?; + if new_accessible > self.mmap.len() - self.offset_guard_size - self.pre_guard_size { + // If the new size of this heap exceeds the current size of the + // allocation we have, then this must be a dynamic heap. Use + // `new_size` to calculate a new size of an allocation, allocate it, + // and then copy over the memory from before. + let request_bytes = self + .pre_guard_size + .checked_add(new_accessible) + .and_then(|s| s.checked_add(self.extra_to_reserve_on_growth)) + .and_then(|s| s.checked_add(self.offset_guard_size)) + .ok_or_else(|| format_err!("overflow calculating size of memory allocation"))?; + assert!(usize_is_multiple_of_host_page_size(request_bytes)); + + let mut new_mmap = Mmap::accessible_reserved(0, request_bytes)?; + new_mmap.make_accessible(self.pre_guard_size, new_accessible)?; + + // This method has an exclusive reference to `self.mmap` and just + // created `new_mmap` so it should be safe to acquire references + // into both of them and copy between them. + unsafe { + let range = self.pre_guard_size..self.pre_guard_size + self.len; + let src = self.mmap.slice(range.clone()); + let dst = new_mmap.slice_mut(range); + dst.copy_from_slice(src); + } + + self.mmap = new_mmap; + } else { + // If the new size of this heap fits within the existing allocation + // then all we need to do is to make the new pages accessible. This + // can happen either for "static" heaps which always hit this case, + // or "dynamic" heaps which have some space reserved after the + // initial allocation to grow into before the heap is moved in + // memory. + assert!(new_size > self.len); + assert!(self.maximum.map_or(true, |max| new_size <= max)); + assert!(new_size <= self.mmap.len() - self.offset_guard_size - self.pre_guard_size); + + let new_accessible = round_usize_up_to_host_pages(new_size)?; + assert!( + new_accessible <= self.mmap.len() - self.offset_guard_size - self.pre_guard_size, + ); + + // If the Wasm memory's page size is smaller than the host's page + // size, then we might not need to actually change permissions, + // since we are forced to round our accessible range up to the + // host's page size. + if new_accessible > self.accessible() { + self.mmap.make_accessible( + self.pre_guard_size + self.accessible(), + new_accessible - self.accessible(), + )?; + } + } + + self.len = new_size; + + Ok(()) + } + + fn set_byte_size(&mut self, len: usize) { + self.len = len; + } + + fn base_ptr(&self) -> *mut u8 { + unsafe { self.mmap.as_mut_ptr().add(self.pre_guard_size) } + } +} diff --git a/crates/wasmtime/src/runtime/vm/threads/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs similarity index 99% rename from crates/wasmtime/src/runtime/vm/threads/shared_memory.rs rename to crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 05642223c6ec..6412c325694d 100644 --- a/crates/wasmtime/src/runtime/vm/threads/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::runtime::vm::memory::{validate_atomic_addr, LocalMemory, MmapMemory}; -use crate::runtime::vm::threads::parking_spot::{ParkingSpot, Waiter}; +use crate::runtime::vm::parking_spot::{ParkingSpot, Waiter}; use crate::runtime::vm::vmcontext::VMMemoryDefinition; use crate::runtime::vm::{Memory, VMStore, WaitResult}; use std::cell::RefCell; diff --git a/crates/wasmtime/src/runtime/vm/threads/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs similarity index 100% rename from crates/wasmtime/src/runtime/vm/threads/shared_memory_disabled.rs rename to crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs diff --git a/crates/wasmtime/src/runtime/vm/memory/static_.rs b/crates/wasmtime/src/runtime/vm/memory/static_.rs new file mode 100644 index 000000000000..ba2f432e46d4 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/memory/static_.rs @@ -0,0 +1,79 @@ +//! Support for implementing the [`RuntimeLinearMemory`] trait in terms of a +//! fixed allocation that cannot move. + +use crate::prelude::*; +use crate::runtime::vm::memory::RuntimeLinearMemory; +use crate::runtime::vm::SendSyncPtr; +use core::ptr::NonNull; + +/// A "static" memory where the lifetime of the backing memory is managed +/// elsewhere. Currently used with the pooling allocator. +pub struct StaticMemory { + /// The base pointer of this static memory, wrapped up in a send/sync + /// wrapper. + base: SendSyncPtr, + + /// The byte capacity of the `base` pointer. + capacity: usize, + + /// The current size, in bytes, of this memory. + size: usize, +} + +impl StaticMemory { + pub fn new( + base_ptr: *mut u8, + base_capacity: usize, + initial_size: usize, + maximum_size: Option, + ) -> Result { + if base_capacity < initial_size { + bail!( + "initial memory size of {} exceeds the pooling allocator's \ + configured maximum memory size of {} bytes", + initial_size, + base_capacity, + ); + } + + // Only use the part of the slice that is necessary. + let base_capacity = match maximum_size { + Some(max) if max < base_capacity => max, + _ => base_capacity, + }; + + Ok(Self { + base: SendSyncPtr::new(NonNull::new(base_ptr).unwrap()), + capacity: base_capacity, + size: initial_size, + }) + } +} + +impl RuntimeLinearMemory for StaticMemory { + fn byte_size(&self) -> usize { + self.size + } + + fn byte_capacity(&self) -> usize { + self.capacity + } + + fn grow_to(&mut self, new_byte_size: usize) -> Result<()> { + // Never exceed the static memory size; this check should have been made + // prior to arriving here. + assert!(new_byte_size <= self.capacity); + + // Update our accounting of the available size. + self.size = new_byte_size; + Ok(()) + } + + fn set_byte_size(&mut self, len: usize) { + self.size = len; + } + + fn base_ptr(&self) -> *mut u8 { + self.base.as_ptr() + } +} diff --git a/crates/wasmtime/src/runtime/vm/mmap.rs b/crates/wasmtime/src/runtime/vm/mmap.rs index 8942637e8a59..78bead698174 100644 --- a/crates/wasmtime/src/runtime/vm/mmap.rs +++ b/crates/wasmtime/src/runtime/vm/mmap.rs @@ -156,11 +156,6 @@ impl Mmap { self.sys.len() } - /// Return whether any memory has been allocated or reserved. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - /// Makes the specified `range` within this `Mmap` to be read/execute. /// /// # Unsafety diff --git a/crates/wasmtime/src/runtime/vm/mmap_vec.rs b/crates/wasmtime/src/runtime/vm/mmap_vec.rs index 4999cbbbb35b..21e45553c2da 100644 --- a/crates/wasmtime/src/runtime/vm/mmap_vec.rs +++ b/crates/wasmtime/src/runtime/vm/mmap_vec.rs @@ -1,23 +1,35 @@ use crate::prelude::*; +#[cfg(feature = "signals-based-traps")] use crate::runtime::vm::Mmap; use alloc::sync::Arc; -use core::ops::{Deref, DerefMut, Range}; +use core::ops::{Deref, Range}; #[cfg(feature = "std")] use std::fs::File; -/// A type akin to `Vec`, but backed by `mmap` and able to be split. +/// A type which prefers to store backing memory in an OS-backed memory mapping +/// but can fall back to `Vec` as well. /// -/// This type is a non-growable owned list of bytes. It can be segmented into -/// disjoint separately owned views akin to the `split_at` method on slices in -/// Rust. An `MmapVec` is backed by an OS-level memory allocation and is not -/// suitable for lots of small allocation (since it works at the page -/// granularity). +/// This type is used to store code in Wasmtime and manage read-only and +/// executable permissions of compiled images. This is created from either an +/// in-memory compilation or by deserializing an artifact from disk. Methods +/// are provided for managing VM permissions when the `signals-based-traps` +/// Cargo feature is enabled. /// -/// An `MmapVec` is an owned value which means that owners have the ability to -/// get exclusive access to the underlying bytes, enabling mutation. -pub struct MmapVec { - mmap: Arc, - range: Range, +/// The length of an `MmapVec` is not guaranteed to be page-aligned. That means +/// that if the contents are not themselves page-aligned, which compiled images +/// are typically not, then the remaining bytes in the final page for +/// mmap-backed instances are unused. +/// +/// Note that when `signals-based-traps` is disabled then this type is backed +/// by a normal `Vec`. In such a scenario this type does not support +/// read-only or executable bits and the methods are not available. +pub enum MmapVec { + #[doc(hidden)] + #[cfg(not(feature = "signals-based-traps"))] + Vec(Vec), + #[doc(hidden)] + #[cfg(feature = "signals-based-traps")] + Mmap { mmap: Mmap, len: usize }, } impl MmapVec { @@ -26,12 +38,15 @@ impl MmapVec { /// The returned `MmapVec` will have the `size` specified, which can be /// smaller than the region mapped by the `Mmap`. The returned `MmapVec` /// will only have at most `size` bytes accessible. - pub fn new(mmap: Mmap, size: usize) -> MmapVec { - assert!(size <= mmap.len()); - MmapVec { - mmap: Arc::new(mmap), - range: 0..size, - } + #[cfg(feature = "signals-based-traps")] + fn new_mmap(mmap: Mmap, len: usize) -> MmapVec { + assert!(len <= mmap.len()); + MmapVec::Mmap { mmap, len } + } + + #[cfg(not(feature = "signals-based-traps"))] + fn new_vec(vec: Vec) -> MmapVec { + MmapVec::Vec(vec) } /// Creates a new zero-initialized `MmapVec` with the given `size`. @@ -40,7 +55,10 @@ impl MmapVec { /// bytes. All bytes will be initialized to zero since this is a fresh OS /// page allocation. pub fn with_capacity(size: usize) -> Result { - Ok(MmapVec::new(Mmap::with_at_least(size)?, size)) + #[cfg(feature = "signals-based-traps")] + return Ok(MmapVec::new_mmap(Mmap::with_at_least(size)?, size)); + #[cfg(not(feature = "signals-based-traps"))] + return Ok(MmapVec::new_vec(vec![0; size])); } /// Creates a new `MmapVec` from the contents of an existing `slice`. @@ -50,7 +68,11 @@ impl MmapVec { /// method if possible to avoid the need to copy data around. pub fn from_slice(slice: &[u8]) -> Result { let mut result = MmapVec::with_capacity(slice.len())?; - result.copy_from_slice(slice); + // SAFETY: The mmap hasn't been made readonly yet so this should be + // safe to call. + unsafe { + result.as_mut_slice().copy_from_slice(slice); + } Ok(result) } @@ -59,47 +81,54 @@ impl MmapVec { /// This function will determine the file's size and map the full contents /// into memory. This will return an error if the file is too large to be /// fully mapped into memory. + /// + /// The file is mapped into memory with a "private mapping" meaning that + /// changes are not persisted back to the file itself and are only visible + /// within this process. #[cfg(feature = "std")] pub fn from_file(file: File) -> Result { let file = Arc::new(file); let mmap = Mmap::from_file(Arc::clone(&file)) .with_context(move || format!("failed to create mmap for file {file:?}"))?; let len = mmap.len(); - Ok(MmapVec::new(mmap, len)) + Ok(MmapVec::new_mmap(mmap, len)) } /// Makes the specified `range` within this `mmap` to be read/execute. + #[cfg(feature = "signals-based-traps")] pub unsafe fn make_executable( &self, range: Range, enable_branch_protection: bool, ) -> Result<()> { + let (mmap, len) = match self { + MmapVec::Mmap { mmap, len } => (mmap, *len), + }; assert!(range.start <= range.end); - assert!(range.end <= self.range.len()); - self.mmap.make_executable( - range.start + self.range.start..range.end + self.range.start, - enable_branch_protection, - ) + assert!(range.end <= len); + mmap.make_executable(range.start..range.end, enable_branch_protection) } /// Makes the specified `range` within this `mmap` to be read-only. + #[cfg(feature = "signals-based-traps")] pub unsafe fn make_readonly(&self, range: Range) -> Result<()> { + let (mmap, len) = match self { + MmapVec::Mmap { mmap, len } => (mmap, *len), + }; assert!(range.start <= range.end); - assert!(range.end <= self.range.len()); - self.mmap - .make_readonly(range.start + self.range.start..range.end + self.range.start) + assert!(range.end <= len); + mmap.make_readonly(range.start..range.end) } /// Returns the underlying file that this mmap is mapping, if present. #[cfg(feature = "std")] pub fn original_file(&self) -> Option<&Arc> { - self.mmap.original_file() - } - - /// Returns the offset within the original mmap that this `MmapVec` is - /// created from. - pub fn original_offset(&self) -> usize { - self.range.start + match self { + #[cfg(not(feature = "signals-based-traps"))] + MmapVec::Vec(_) => None, + #[cfg(feature = "signals-based-traps")] + MmapVec::Mmap { mmap, .. } => mmap.original_file(), + } } /// Returns the bounds, in host memory, of where this mmap @@ -109,6 +138,21 @@ impl MmapVec { let len = self.len(); base..base.wrapping_add(len) } + + /// Views this region of memory as a mutable slice. + /// + /// # Unsafety + /// + /// This method is only safe if `make_readonly` hasn't been called yet to + /// ensure that the memory is indeed writable + pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] { + match self { + #[cfg(not(feature = "signals-based-traps"))] + MmapVec::Vec(v) => v, + #[cfg(feature = "signals-based-traps")] + MmapVec::Mmap { mmap, len } => mmap.slice_mut(0..*len), + } + } } impl Deref for MmapVec { @@ -116,25 +160,15 @@ impl Deref for MmapVec { #[inline] fn deref(&self) -> &[u8] { - // SAFETY: this mmap owns its own range of the underlying mmap so it - // should be all good-to-read. - unsafe { self.mmap.slice(self.range.clone()) } - } -} - -impl DerefMut for MmapVec { - fn deref_mut(&mut self) -> &mut [u8] { - // SAFETY: The underlying mmap is protected behind an `Arc` which means - // there there can be many references to it. We are guaranteed, though, - // that each reference to the underlying `mmap` has a disjoint `range` - // listed that it can access. This means that despite having shared - // access to the mmap itself we have exclusive ownership of the bytes - // specified in `self.range`. This should allow us to safely hand out - // mutable access to these bytes if so desired. - unsafe { - let slice = - core::slice::from_raw_parts_mut(self.mmap.as_ptr().cast_mut(), self.mmap.len()); - &mut slice[self.range.clone()] + match self { + #[cfg(not(feature = "signals-based-traps"))] + MmapVec::Vec(v) => v, + #[cfg(feature = "signals-based-traps")] + MmapVec::Mmap { mmap, len } => { + // SAFETY: all bytes for this mmap, which is owned by + // `MmapVec`, are always at least readable. + unsafe { mmap.slice(0..*len) } + } } } } @@ -149,8 +183,10 @@ mod tests { assert_eq!(mmap.len(), 10); assert_eq!(&mmap[..], &[0; 10]); - mmap[0] = 1; - mmap[2] = 3; + unsafe { + mmap.as_mut_slice()[0] = 1; + mmap.as_mut_slice()[2] = 3; + } assert!(mmap.get(10).is_none()); assert_eq!(mmap[0], 1); assert_eq!(mmap[2], 3); diff --git a/crates/wasmtime/src/runtime/vm/threads/parking_spot.rs b/crates/wasmtime/src/runtime/vm/parking_spot.rs similarity index 100% rename from crates/wasmtime/src/runtime/vm/threads/parking_spot.rs rename to crates/wasmtime/src/runtime/vm/parking_spot.rs diff --git a/crates/wasmtime/src/runtime/vm/sys/custom/capi.rs b/crates/wasmtime/src/runtime/vm/sys/custom/capi.rs index b63280fa5a51..1745f2d6f25d 100644 --- a/crates/wasmtime/src/runtime/vm/sys/custom/capi.rs +++ b/crates/wasmtime/src/runtime/vm/sys/custom/capi.rs @@ -3,14 +3,20 @@ // Flags to either `wasmtime_mmap_{new,remap}` or `wasmtime_mprotect`. /// Indicates that the memory region should be readable. +#[cfg(feature = "signals-based-traps")] pub const WASMTIME_PROT_READ: u32 = 1 << 0; /// Indicates that the memory region should be writable. +#[cfg(feature = "signals-based-traps")] pub const WASMTIME_PROT_WRITE: u32 = 1 << 1; /// Indicates that the memory region should be executable. +#[cfg(feature = "signals-based-traps")] pub const WASMTIME_PROT_EXEC: u32 = 1 << 2; +#[cfg(feature = "signals-based-traps")] pub use WASMTIME_PROT_EXEC as PROT_EXEC; +#[cfg(feature = "signals-based-traps")] pub use WASMTIME_PROT_READ as PROT_READ; +#[cfg(feature = "signals-based-traps")] pub use WASMTIME_PROT_WRITE as PROT_WRITE; /// Handler function for traps in Wasmtime passed to `wasmtime_init_traps`. @@ -37,11 +43,13 @@ pub use WASMTIME_PROT_WRITE as PROT_WRITE; /// /// When this function does not return it's because `wasmtime_longjmp` is /// used to handle a Wasm-based trap. +#[cfg(feature = "signals-based-traps")] pub type wasmtime_trap_handler_t = extern "C" fn(ip: usize, fp: usize, has_faulting_addr: bool, faulting_addr: usize); /// Abstract pointer type used in the `wasmtime_memory_image_*` APIs which /// is defined by the embedder. +#[cfg(feature = "signals-based-traps")] pub enum wasmtime_memory_image {} extern "C" { @@ -55,6 +63,7 @@ extern "C" { /// Returns 0 on success and an error code on failure. /// /// Similar to `mmap(0, size, prot_flags, MAP_PRIVATE, 0, -1)` on Linux. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_mmap_new(size: usize, prot_flags: u32, ret: &mut *mut u8) -> i32; /// Remaps the virtual memory starting at `addr` going for `size` bytes to @@ -67,6 +76,7 @@ extern "C" { /// Returns 0 on success and an error code on failure. /// /// Similar to `mmap(addr, size, prot_flags, MAP_PRIVATE | MAP_FIXED, 0, -1)` on Linux. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_mmap_remap(addr: *mut u8, size: usize, prot_flags: u32) -> i32; /// Unmaps memory at the specified `ptr` for `size` bytes. @@ -77,6 +87,7 @@ extern "C" { /// Returns 0 on success and an error code on failure. /// /// Similar to `munmap` on Linux. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_munmap(ptr: *mut u8, size: usize) -> i32; /// Configures the protections associated with a region of virtual memory @@ -85,9 +96,11 @@ extern "C" { /// Returns 0 on success and an error code on failure. /// /// Similar to `mprotect` on Linux. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_mprotect(ptr: *mut u8, size: usize, prot_flags: u32) -> i32; /// Returns the page size, in bytes, of the current system. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_page_size() -> usize; /// Used to setup a frame on the stack to longjmp back to in the future. @@ -134,6 +147,7 @@ extern "C" { /// the system. /// /// Returns 0 on success and an error code on failure. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_init_traps(handler: wasmtime_trap_handler_t) -> i32; /// Attempts to create a new in-memory image of the `ptr`/`len` combo which @@ -155,6 +169,7 @@ extern "C" { /// `NULL` into `ret` is not considered a failure, and failure is used to /// indicate that something fatal has happened and Wasmtime will propagate /// the error upwards. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_memory_image_new( ptr: *const u8, len: usize, @@ -175,6 +190,7 @@ extern "C" { /// the future. /// /// Aborts the process on failure. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_memory_image_map_at( image: *mut wasmtime_memory_image, addr: *mut u8, @@ -185,6 +201,7 @@ extern "C" { /// /// Note that mappings created from this image are not guaranteed to be /// deallocated and/or unmapped before this is called. + #[cfg(feature = "signals-based-traps")] pub fn wasmtime_memory_image_free(image: *mut wasmtime_memory_image); /// Wasmtime requires a single pointer's space of TLS to be used at runtime, diff --git a/crates/wasmtime/src/runtime/vm/sys/custom/mod.rs b/crates/wasmtime/src/runtime/vm/sys/custom/mod.rs index 271479165ca2..a810764df590 100644 --- a/crates/wasmtime/src/runtime/vm/sys/custom/mod.rs +++ b/crates/wasmtime/src/runtime/vm/sys/custom/mod.rs @@ -8,14 +8,20 @@ //! For more information about this see `./examples/min-platform` as well as //! `./docs/examples-minimal.md`. +#![warn(dead_code, unused_imports)] + +#[cfg(feature = "signals-based-traps")] use crate::prelude::*; pub mod capi; +#[cfg(feature = "signals-based-traps")] pub mod mmap; pub mod traphandlers; pub mod unwind; +#[cfg(feature = "signals-based-traps")] pub mod vm; +#[cfg(feature = "signals-based-traps")] fn cvt(rc: i32) -> Result<()> { match rc { 0 => Ok(()), diff --git a/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs index 72f65bf0e65d..0344ac98bad9 100644 --- a/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs @@ -1,11 +1,11 @@ -use crate::runtime::vm::traphandlers::{tls, TrapRegisters, TrapTest}; +use crate::prelude::*; use crate::runtime::vm::VMContext; use core::mem; pub use crate::runtime::vm::sys::capi::{self, wasmtime_longjmp}; #[allow(missing_docs)] -pub type SignalHandler<'a> = dyn Fn() + Send + Sync + 'a; +pub type SignalHandler = Box; pub unsafe fn wasmtime_setjmp( jmp_buf: *mut *const u8, @@ -20,8 +20,10 @@ pub unsafe fn wasmtime_setjmp( capi::wasmtime_setjmp(jmp_buf, callback, payload, callee.cast()) } +#[cfg(feature = "signals-based-traps")] pub struct TrapHandler; +#[cfg(feature = "signals-based-traps")] impl TrapHandler { pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler { capi::wasmtime_init_traps(handle_trap); @@ -31,7 +33,10 @@ impl TrapHandler { pub fn validate_config(&self, _macos_use_mach_ports: bool) {} } +#[cfg(feature = "signals-based-traps")] extern "C" fn handle_trap(pc: usize, fp: usize, has_faulting_addr: bool, faulting_addr: usize) { + use crate::runtime::vm::traphandlers::{tls, TrapRegisters, TrapTest}; + tls::with(|info| { let info = match info { Some(info) => info, diff --git a/crates/wasmtime/src/runtime/vm/sys/custom/vm.rs b/crates/wasmtime/src/runtime/vm/sys/custom/vm.rs index e7f79e41c701..1a1f1e015c97 100644 --- a/crates/wasmtime/src/runtime/vm/sys/custom/vm.rs +++ b/crates/wasmtime/src/runtime/vm/sys/custom/vm.rs @@ -30,6 +30,7 @@ pub unsafe fn commit_pages(_addr: *mut u8, _len: usize) -> Result<()> { Ok(()) } +#[cfg(feature = "pooling-allocator")] pub unsafe fn decommit_pages(addr: *mut u8, len: usize) -> Result<()> { if len == 0 { return Ok(()); diff --git a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs index 0059e4b0654c..a1f4cae050b6 100644 --- a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs @@ -9,6 +9,7 @@ // Note that no actual JIT code runs in MIRI so this is purely here for // host-to-host calls. +use crate::prelude::*; use crate::runtime::vm::VMContext; pub fn wasmtime_setjmp( @@ -28,21 +29,6 @@ pub fn wasmtime_longjmp(_jmp_buf: *const u8) -> ! { } #[allow(missing_docs)] -pub type SignalHandler<'a> = dyn Fn() + Send + Sync + 'a; - -pub struct TrapHandler; - -impl TrapHandler { - pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler { - TrapHandler - } - - pub fn validate_config(&self, _macos_use_mach_ports: bool) {} -} +pub type SignalHandler = Box; pub fn lazy_per_thread_init() {} - -#[cfg(target_os = "macos")] -pub fn using_mach_ports() -> bool { - false -} diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs deleted file mode 100644 index 862b3eb44dd7..000000000000 --- a/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs +++ /dev/null @@ -1,39 +0,0 @@ -/// Whether or not macOS is using mach ports. -#[cfg(target_os = "macos")] -static mut USE_MACH_PORTS: bool = false; - -pub use super::signals::{wasmtime_longjmp, wasmtime_setjmp, SignalHandler}; - -pub enum TrapHandler { - Signals(super::signals::TrapHandler), - #[allow(dead_code)] // used for its drop - MachPorts(super::machports::TrapHandler), -} - -impl TrapHandler { - pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler { - USE_MACH_PORTS = macos_use_mach_ports; - if macos_use_mach_ports { - TrapHandler::MachPorts(super::machports::TrapHandler::new()) - } else { - TrapHandler::Signals(super::signals::TrapHandler::new(false)) - } - } - - pub fn validate_config(&self, macos_use_mach_ports: bool) { - match self { - TrapHandler::Signals(t) => t.validate_config(macos_use_mach_ports), - TrapHandler::MachPorts(_) => assert!(macos_use_mach_ports), - } - } -} - -pub fn lazy_per_thread_init() { - unsafe { - if USE_MACH_PORTS { - super::machports::lazy_per_thread_init(); - } else { - super::signals::lazy_per_thread_init(); - } - } -} diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/mod.rs b/crates/wasmtime/src/runtime/vm/sys/unix/mod.rs index 92bcbc9149a0..3c8fbcccae5d 100644 --- a/crates/wasmtime/src/runtime/vm/sys/unix/mod.rs +++ b/crates/wasmtime/src/runtime/vm/sys/unix/mod.rs @@ -5,23 +5,18 @@ use core::cell::Cell; +#[cfg(feature = "signals-based-traps")] pub mod mmap; +pub mod traphandlers; pub mod unwind; +#[cfg(feature = "signals-based-traps")] pub mod vm; +#[cfg(all(feature = "signals-based-traps", target_os = "macos"))] +pub mod machports; +#[cfg(feature = "signals-based-traps")] pub mod signals; -cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { - pub mod machports; - - pub mod macos_traphandlers; - pub use macos_traphandlers as traphandlers; - } else { - pub use signals as traphandlers; - } -} - std::thread_local!(static TLS: Cell<*mut u8> = const { Cell::new(std::ptr::null_mut()) }); #[inline] diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs index 4f1142a108de..e02570a04592 100644 --- a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs +++ b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs @@ -1,30 +1,16 @@ //! Trap handling on Unix based on POSIX signals. +use crate::prelude::*; +use crate::runtime::vm::sys::traphandlers::wasmtime_longjmp; use crate::runtime::vm::traphandlers::{tls, TrapRegisters, TrapTest}; -use crate::runtime::vm::VMContext; use std::cell::RefCell; use std::io; use std::mem; use std::ptr::{self, addr_of, addr_of_mut, null_mut}; -#[link(name = "wasmtime-helpers")] -extern "C" { - #[wasmtime_versioned_export_macros::versioned_link] - #[allow(improper_ctypes)] - pub fn wasmtime_setjmp( - jmp_buf: *mut *const u8, - callback: extern "C" fn(*mut u8, *mut VMContext), - payload: *mut u8, - callee: *mut VMContext, - ) -> i32; - - #[wasmtime_versioned_export_macros::versioned_link] - pub fn wasmtime_longjmp(jmp_buf: *const u8) -> !; -} - /// Function which may handle custom signals while processing traps. -pub type SignalHandler<'a> = - dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool + Send + Sync + 'a; +pub type SignalHandler = + Box bool + Send + Sync>; const UNINIT_SIGACTION: libc::sigaction = unsafe { mem::zeroed() }; static mut PREV_SIGSEGV: libc::sigaction = UNINIT_SIGACTION; diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/unix/traphandlers.rs new file mode 100644 index 000000000000..ffd179d49ac0 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/sys/unix/traphandlers.rs @@ -0,0 +1,74 @@ +use crate::vm::VMContext; + +#[link(name = "wasmtime-helpers")] +extern "C" { + #[wasmtime_versioned_export_macros::versioned_link] + #[allow(improper_ctypes)] + pub fn wasmtime_setjmp( + jmp_buf: *mut *const u8, + callback: extern "C" fn(*mut u8, *mut VMContext), + payload: *mut u8, + callee: *mut VMContext, + ) -> i32; + + #[wasmtime_versioned_export_macros::versioned_link] + pub fn wasmtime_longjmp(jmp_buf: *const u8) -> !; +} + +cfg_if::cfg_if! { + if #[cfg(not(feature = "signals-based-traps"))] { + // If signals-based traps are disabled statically then there's no + // platform signal handler and no per-thread init, so stub these both + // out. + pub enum SignalHandler {} + + #[inline] + pub fn lazy_per_thread_init() {} + } else if #[cfg(target_os = "macos")] { + // On macOS a dynamic decision is made to use mach ports or signals at + // process initialization time. + + /// Whether or not macOS is using mach ports. + static mut USE_MACH_PORTS: bool = false; + + pub use super::signals::SignalHandler; + + pub enum TrapHandler { + Signals(super::signals::TrapHandler), + #[allow(dead_code)] // used for its drop + MachPorts(super::machports::TrapHandler), + } + + impl TrapHandler { + pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler { + USE_MACH_PORTS = macos_use_mach_ports; + if macos_use_mach_ports { + TrapHandler::MachPorts(super::machports::TrapHandler::new()) + } else { + TrapHandler::Signals(super::signals::TrapHandler::new(false)) + } + } + + pub fn validate_config(&self, macos_use_mach_ports: bool) { + match self { + TrapHandler::Signals(t) => t.validate_config(macos_use_mach_ports), + TrapHandler::MachPorts(_) => assert!(macos_use_mach_ports), + } + } + } + + pub fn lazy_per_thread_init() { + unsafe { + if USE_MACH_PORTS { + super::machports::lazy_per_thread_init(); + } else { + super::signals::lazy_per_thread_init(); + } + } + } + } else { + // Otherwise unix platforms use the signals-based implementation of + // these functions. + pub use super::signals::{TrapHandler, SignalHandler, lazy_per_thread_init}; + } +} diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/unwind.rs b/crates/wasmtime/src/runtime/vm/sys/unix/unwind.rs index 6615e08926e0..cf7a1d65203e 100644 --- a/crates/wasmtime/src/runtime/vm/sys/unix/unwind.rs +++ b/crates/wasmtime/src/runtime/vm/sys/unix/unwind.rs @@ -55,6 +55,7 @@ impl UnwindRegistration { unwind_info: *const u8, unwind_len: usize, ) -> Result { + #[cfg(feature = "signals-based-traps")] debug_assert_eq!( unwind_info as usize % crate::runtime::vm::host_page_size(), 0, diff --git a/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs index 69e1cb90fcb7..e331299d9ffc 100644 --- a/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs @@ -1,3 +1,4 @@ +use crate::prelude::*; use crate::runtime::vm::traphandlers::{tls, TrapRegisters, TrapTest}; use crate::runtime::vm::VMContext; use std::ffi::c_void; @@ -22,7 +23,7 @@ extern "C" { } /// Function which may handle custom signals while processing traps. -pub type SignalHandler<'a> = dyn Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync + 'a; +pub type SignalHandler = Box bool + Send + Sync>; pub struct TrapHandler { handle: *mut c_void, diff --git a/crates/wasmtime/src/runtime/vm/threads/mod.rs b/crates/wasmtime/src/runtime/vm/threads/mod.rs deleted file mode 100644 index dad96f471870..000000000000 --- a/crates/wasmtime/src/runtime/vm/threads/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(feature = "threads")] -mod parking_spot; - -#[cfg(feature = "threads")] -mod shared_memory; -#[cfg(feature = "threads")] -pub use shared_memory::SharedMemory; - -#[cfg(not(feature = "threads"))] -mod shared_memory_disabled; -#[cfg(not(feature = "threads"))] -pub use shared_memory_disabled::SharedMemory; diff --git a/crates/wasmtime/src/runtime/vm/traphandlers.rs b/crates/wasmtime/src/runtime/vm/traphandlers.rs index d9b9a326e857..79b47a660496 100644 --- a/crates/wasmtime/src/runtime/vm/traphandlers.rs +++ b/crates/wasmtime/src/runtime/vm/traphandlers.rs @@ -10,11 +10,14 @@ mod coredump; #[path = "traphandlers/coredump_disabled.rs"] mod coredump; +#[cfg(all(feature = "signals-based-traps", not(miri)))] +mod signals; +#[cfg(all(feature = "signals-based-traps", not(miri)))] +pub use self::signals::*; + use crate::prelude::*; -use crate::runtime::module::lookup_code; use crate::runtime::vm::sys::traphandlers; use crate::runtime::vm::{Instance, VMContext, VMRuntimeLimits}; -use crate::sync::RwLock; use core::cell::{Cell, UnsafeCell}; use core::mem::MaybeUninit; use core::ops::Range; @@ -26,60 +29,6 @@ pub use self::tls::{tls_eager_initialize, AsyncWasmCallState, PreviousAsyncWasmC pub use traphandlers::SignalHandler; -/// Platform-specific trap-handler state. -/// -/// This state is protected by a lock to synchronize access to it. Right now -/// it's a `RwLock` but it could be a `Mutex`, and `RwLock` is just chosen for -/// convenience as it's what's implemented in no_std. The performance here -/// should not be of consequence. -/// -/// This is initialized to `None` and then set as part of `init_traps`. -static TRAP_HANDLER: RwLock> = RwLock::new(None); - -/// This function is required to be called before any WebAssembly is entered. -/// This will configure global state such as signal handlers to prepare the -/// process to receive wasm traps. -/// -/// # Panics -/// -/// This function will panic on macOS if it is called twice or more times with -/// different values of `macos_use_mach_ports`. -/// -/// This function will also panic if the `std` feature is disabled and it's -/// called concurrently. -pub fn init_traps(macos_use_mach_ports: bool) { - let mut lock = TRAP_HANDLER.write(); - match lock.as_mut() { - Some(state) => state.validate_config(macos_use_mach_ports), - None => *lock = Some(unsafe { traphandlers::TrapHandler::new(macos_use_mach_ports) }), - } -} - -/// De-initializes platform-specific state for trap handling. -/// -/// # Panics -/// -/// This function will also panic if the `std` feature is disabled and it's -/// called concurrently. -/// -/// # Aborts -/// -/// This may abort the process on some platforms where trap handling state -/// cannot be unloaded. -/// -/// # Unsafety -/// -/// This is not safe to be called unless all wasm code is unloaded. This is not -/// safe to be called on some platforms, like Unix, when other libraries -/// installed their own signal handlers after `init_traps` was called. -/// -/// There's more reasons for unsafety here than those articulated above, -/// generally this can only be called "if you know what you're doing". -pub unsafe fn deinit_traps() { - let mut lock = TRAP_HANDLER.write(); - let _ = lock.take(); -} - fn lazy_per_thread_init() { traphandlers::lazy_per_thread_init(); } @@ -172,6 +121,7 @@ pub enum TrapReason { }, /// A trap raised from Cranelift-generated code. + #[cfg(all(feature = "signals-based-traps", not(miri)))] Jit { /// The program counter where this trap originated. /// @@ -220,31 +170,12 @@ impl From for TrapReason { } } -pub(crate) struct TrapRegisters { - pub pc: usize, - pub fp: usize, -} - -/// Return value from `test_if_trap`. -pub(crate) enum TrapTest { - /// Not a wasm trap, need to delegate to whatever process handler is next. - NotWasm, - /// This trap was handled by the embedder via custom embedding APIs. - HandledByEmbedder, - /// This is a wasm trap, it needs to be handled. - #[cfg_attr(miri, allow(dead_code))] - Trap { - /// How to longjmp back to the original wasm frame. - jmp_buf: *const u8, - }, -} - /// Catches any wasm traps that happen within the execution of `closure`, /// returning them as a `Result`. /// /// Highly unsafe since `closure` won't have any dtors run. pub unsafe fn catch_traps( - signal_handler: Option<*const SignalHandler<'static>>, + signal_handler: Option<*const SignalHandler>, capture_backtrace: bool, capture_coredump: bool, async_guard_range: Range<*mut u8>, @@ -302,7 +233,8 @@ mod call_thread_state { pub(super) unwind: UnsafeCell, Option)>>, pub(super) jmp_buf: Cell<*const u8>, - pub(super) signal_handler: Option<*const SignalHandler<'static>>, + #[cfg(all(feature = "signals-based-traps", not(miri)))] + pub(super) signal_handler: Option<*const SignalHandler>, pub(super) capture_backtrace: bool, #[cfg(feature = "coredump")] pub(super) capture_coredump: bool, @@ -310,7 +242,7 @@ mod call_thread_state { pub(crate) limits: *const VMRuntimeLimits, pub(super) prev: Cell, - #[cfg_attr(any(windows, miri), allow(dead_code))] + #[cfg(all(feature = "signals-based-traps", unix, not(miri)))] pub(crate) async_guard_range: Range<*mut u8>, // The values of `VMRuntimeLimits::last_wasm_{exit_{pc,fp},entry_sp}` @@ -339,22 +271,24 @@ mod call_thread_state { impl CallThreadState { #[inline] pub(super) fn new( - signal_handler: Option<*const SignalHandler<'static>>, + signal_handler: Option<*const SignalHandler>, capture_backtrace: bool, capture_coredump: bool, limits: *const VMRuntimeLimits, async_guard_range: Range<*mut u8>, ) -> CallThreadState { - let _ = capture_coredump; + let _ = (capture_coredump, signal_handler, &async_guard_range); CallThreadState { unwind: UnsafeCell::new(MaybeUninit::uninit()), jmp_buf: Cell::new(ptr::null()), + #[cfg(all(feature = "signals-based-traps", not(miri)))] signal_handler, capture_backtrace, #[cfg(feature = "coredump")] capture_coredump, limits, + #[cfg(all(feature = "signals-based-traps", unix, not(miri)))] async_guard_range, prev: Cell::new(ptr::null()), old_last_wasm_exit_fp: Cell::new(unsafe { *(*limits).last_wasm_exit_fp.get() }), @@ -452,86 +386,6 @@ impl CallThreadState { } } - /// Trap handler using our thread-local state. - /// - /// * `pc` - the program counter the trap happened at - /// * `call_handler` - a closure used to invoke the platform-specific - /// signal handler for each instance, if available. - /// - /// Attempts to handle the trap if it's a wasm trap. Returns a few - /// different things: - /// - /// * null - the trap didn't look like a wasm trap and should continue as a - /// trap - /// * 1 as a pointer - the trap was handled by a custom trap handler on an - /// instance, and the trap handler should quickly return. - /// * a different pointer - a jmp_buf buffer to longjmp to, meaning that - /// the wasm trap was successfully handled. - #[cfg_attr(miri, allow(dead_code))] // miri doesn't handle traps yet - pub(crate) fn test_if_trap( - &self, - regs: TrapRegisters, - faulting_addr: Option, - call_handler: impl Fn(&SignalHandler) -> bool, - ) -> TrapTest { - // If we haven't even started to handle traps yet, bail out. - if self.jmp_buf.get().is_null() { - return TrapTest::NotWasm; - } - - // First up see if any instance registered has a custom trap handler, - // in which case run them all. If anything handles the trap then we - // return that the trap was handled. - if let Some(handler) = self.signal_handler { - if unsafe { call_handler(&*handler) } { - return TrapTest::HandledByEmbedder; - } - } - - // If this fault wasn't in wasm code, then it's not our problem - let Some((code, text_offset)) = lookup_code(regs.pc) else { - return TrapTest::NotWasm; - }; - - let Some(trap) = code.lookup_trap_code(text_offset) else { - return TrapTest::NotWasm; - }; - - self.set_jit_trap(regs, faulting_addr, trap); - - // If all that passed then this is indeed a wasm trap, so return the - // `jmp_buf` passed to `wasmtime_longjmp` to resume. - TrapTest::Trap { - jmp_buf: self.take_jmp_buf(), - } - } - - pub(crate) fn take_jmp_buf(&self) -> *const u8 { - self.jmp_buf.replace(ptr::null()) - } - - #[cfg_attr(miri, allow(dead_code))] // miri doesn't handle traps yet - pub(crate) fn set_jit_trap( - &self, - TrapRegisters { pc, fp, .. }: TrapRegisters, - faulting_addr: Option, - trap: wasmtime_environ::Trap, - ) { - let backtrace = self.capture_backtrace(self.limits, Some((pc, fp))); - let coredump = self.capture_coredump(self.limits, Some((pc, fp))); - unsafe { - (*self.unwind.get()).as_mut_ptr().write(( - UnwindReason::Trap(TrapReason::Jit { - pc, - faulting_addr, - trap, - }), - backtrace, - coredump, - )); - } - } - fn capture_backtrace( &self, limits: *const VMRuntimeLimits, diff --git a/crates/wasmtime/src/runtime/vm/traphandlers/signals.rs b/crates/wasmtime/src/runtime/vm/traphandlers/signals.rs new file mode 100644 index 000000000000..7783b9de2a41 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/traphandlers/signals.rs @@ -0,0 +1,166 @@ +//! Trap handling support when `feature = "signals-based-traps"` is enabled. +//! +//! This module is conditionally included in the above `traphandlers` module and +//! contains support and shared routines for working with signals-based traps. +//! Each platform will have its own `signals-based-traps` configuration and +//! thise module serves as a shared entrypoint for initialization entrypoints +//! (`init_traps`) and testing if a trapping opcode is wasm (`test_if_trap`). + +use crate::runtime::module::lookup_code; +use crate::sync::RwLock; +use crate::vm::sys::traphandlers::TrapHandler; +use crate::vm::traphandlers::{CallThreadState, SignalHandler, TrapReason, UnwindReason}; +use core::ptr; + +pub(crate) struct TrapRegisters { + pub pc: usize, + pub fp: usize, +} + +/// Return value from `test_if_trap`. +pub(crate) enum TrapTest { + /// Not a wasm trap, need to delegate to whatever process handler is next. + NotWasm, + /// This trap was handled by the embedder via custom embedding APIs. + HandledByEmbedder, + /// This is a wasm trap, it needs to be handled. + #[cfg_attr(miri, allow(dead_code))] + Trap { + /// How to longjmp back to the original wasm frame. + jmp_buf: *const u8, + }, +} + +/// Platform-specific trap-handler state. +/// +/// This state is protected by a lock to synchronize access to it. Right now +/// it's a `RwLock` but it could be a `Mutex`, and `RwLock` is just chosen for +/// convenience as it's what's implemented in no_std. The performance here +/// should not be of consequence. +/// +/// This is initialized to `None` and then set as part of `init_traps`. +static TRAP_HANDLER: RwLock> = RwLock::new(None); + +/// This function is required to be called before any WebAssembly is entered. +/// This will configure global state such as signal handlers to prepare the +/// process to receive wasm traps. +/// +/// # Panics +/// +/// This function will panic on macOS if it is called twice or more times with +/// different values of `macos_use_mach_ports`. +/// +/// This function will also panic if the `std` feature is disabled and it's +/// called concurrently. +pub fn init_traps(macos_use_mach_ports: bool) { + let mut lock = TRAP_HANDLER.write(); + match lock.as_mut() { + Some(state) => state.validate_config(macos_use_mach_ports), + None => *lock = Some(unsafe { TrapHandler::new(macos_use_mach_ports) }), + } +} + +/// De-initializes platform-specific state for trap handling. +/// +/// # Panics +/// +/// This function will also panic if the `std` feature is disabled and it's +/// called concurrently. +/// +/// # Aborts +/// +/// This may abort the process on some platforms where trap handling state +/// cannot be unloaded. +/// +/// # Unsafety +/// +/// This is not safe to be called unless all wasm code is unloaded. This is not +/// safe to be called on some platforms, like Unix, when other libraries +/// installed their own signal handlers after `init_traps` was called. +/// +/// There's more reasons for unsafety here than those articulated above, +/// generally this can only be called "if you know what you're doing". +pub unsafe fn deinit_traps() { + let mut lock = TRAP_HANDLER.write(); + let _ = lock.take(); +} + +impl CallThreadState { + /// Trap handler using our thread-local state. + /// + /// * `pc` - the program counter the trap happened at + /// * `call_handler` - a closure used to invoke the platform-specific + /// signal handler for each instance, if available. + /// + /// Attempts to handle the trap if it's a wasm trap. Returns a few + /// different things: + /// + /// * null - the trap didn't look like a wasm trap and should continue as a + /// trap + /// * 1 as a pointer - the trap was handled by a custom trap handler on an + /// instance, and the trap handler should quickly return. + /// * a different pointer - a jmp_buf buffer to longjmp to, meaning that + /// the wasm trap was successfully handled. + pub(crate) fn test_if_trap( + &self, + regs: TrapRegisters, + faulting_addr: Option, + call_handler: impl Fn(&SignalHandler) -> bool, + ) -> TrapTest { + // If we haven't even started to handle traps yet, bail out. + if self.jmp_buf.get().is_null() { + return TrapTest::NotWasm; + } + + // First up see if any instance registered has a custom trap handler, + // in which case run them all. If anything handles the trap then we + // return that the trap was handled. + if let Some(handler) = self.signal_handler { + if unsafe { call_handler(&*handler) } { + return TrapTest::HandledByEmbedder; + } + } + + // If this fault wasn't in wasm code, then it's not our problem + let Some((code, text_offset)) = lookup_code(regs.pc) else { + return TrapTest::NotWasm; + }; + + let Some(trap) = code.lookup_trap_code(text_offset) else { + return TrapTest::NotWasm; + }; + + self.set_jit_trap(regs, faulting_addr, trap); + + // If all that passed then this is indeed a wasm trap, so return the + // `jmp_buf` passed to `wasmtime_longjmp` to resume. + TrapTest::Trap { + jmp_buf: self.take_jmp_buf(), + } + } + + pub(crate) fn take_jmp_buf(&self) -> *const u8 { + self.jmp_buf.replace(ptr::null()) + } + + pub(crate) fn set_jit_trap( + &self, + TrapRegisters { pc, fp, .. }: TrapRegisters, + faulting_addr: Option, + trap: wasmtime_environ::Trap, + ) { + let backtrace = self.capture_backtrace(self.limits, Some((pc, fp))); + let coredump = self.capture_coredump(self.limits, Some((pc, fp))); + unsafe { + (*self.unwind.get()).as_mut_ptr().write(( + UnwindReason::Trap(TrapReason::Jit { + pc, + faulting_addr, + trap, + }), + backtrace, + coredump, + )); + } + } +} diff --git a/crates/wasmtime/src/runtime/windows.rs b/crates/wasmtime/src/runtime/windows.rs index 46022b53803a..b1c018b211d1 100644 --- a/crates/wasmtime/src/runtime/windows.rs +++ b/crates/wasmtime/src/runtime/windows.rs @@ -18,12 +18,14 @@ pub trait StoreExt { /// Configures a custom signal handler to execute. /// /// TODO: needs more documentation. + #[cfg(feature = "signals-based-traps")] unsafe fn set_signal_handler(&mut self, handler: H) where H: 'static + Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync; } impl StoreExt for Store { + #[cfg(feature = "signals-based-traps")] unsafe fn set_signal_handler(&mut self, handler: H) where H: 'static + Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync, diff --git a/examples/min-platform/Cargo.toml b/examples/min-platform/Cargo.toml index 3cb9339ec5b9..9db575077390 100644 --- a/examples/min-platform/Cargo.toml +++ b/examples/min-platform/Cargo.toml @@ -13,3 +13,6 @@ anyhow = { workspace = true, features = ['std'] } libloading = "0.8" object = { workspace = true, features = ['std'] } wasmtime = { workspace = true, features = ['cranelift', 'wat'] } + +[features] +signals-based-traps = [] diff --git a/examples/min-platform/build.sh b/examples/min-platform/build.sh index 3e6f9d1f0449..be4e8d06bcb6 100755 --- a/examples/min-platform/build.sh +++ b/examples/min-platform/build.sh @@ -21,6 +21,11 @@ EMBEDDING_DIR=$HOST_DIR/embedding set -ex +if [ "$WASMTIME_SIGNALS_BASED_TRAPS" = "1" ]; then + cflags="$cflags -DWASMTIME_SIGNALS_BASED_TRAPS" + features="$features,signals-based-traps" +fi + # First compile the C implementation of the platform symbols that will be # required by our embedding. This is the `embedding/wasmtime-platform.c` file. # The header file used is generated from Rust source code with the `cbindgen` @@ -32,7 +37,7 @@ set -ex cbindgen "$REPO_DIR/crates/wasmtime/src/runtime/vm/sys/custom/capi.rs" \ --config "$EMBEDDING_DIR/cbindgen.toml" > "$EMBEDDING_DIR/wasmtime-platform.h" clang -shared -O2 -o "$HOST_DIR/libwasmtime-platform.so" "$EMBEDDING_DIR/wasmtime-platform.c" \ - -D_GNU_SOURCE + -D_GNU_SOURCE $cflags # Next the embedding itself is built. # @@ -44,6 +49,7 @@ clang -shared -O2 -o "$HOST_DIR/libwasmtime-platform.so" "$EMBEDDING_DIR/wasmtim cargo build \ --manifest-path $EMBEDDING_DIR/Cargo.toml \ --target $target \ + --features "$features" \ --release cc \ -Wl,--gc-sections \ @@ -55,7 +61,7 @@ cc \ # The final step here is running the host, in the current directory, which will # load the embedding and execute it. -cargo run --manifest-path "$HOST_DIR/Cargo.toml" --release -- \ +cargo run --manifest-path "$HOST_DIR/Cargo.toml" --release --features "$features" -- \ "$target" \ "$HOST_DIR/libembedding.so" \ "$HOST_DIR/libwasmtime-platform.so" diff --git a/examples/min-platform/embedding/Cargo.toml b/examples/min-platform/embedding/Cargo.toml index e218d897fec4..8e7818a1ff28 100644 --- a/examples/min-platform/embedding/Cargo.toml +++ b/examples/min-platform/embedding/Cargo.toml @@ -23,3 +23,6 @@ dlmalloc = "0.2.4" crate-type = ['staticlib'] test = false doctest = false + +[features] +signals-based-traps = ['wasmtime/signals-based-traps'] diff --git a/examples/min-platform/embedding/cbindgen.toml b/examples/min-platform/embedding/cbindgen.toml index 081d2ae23b1c..79dc2f5fb10c 100644 --- a/examples/min-platform/embedding/cbindgen.toml +++ b/examples/min-platform/embedding/cbindgen.toml @@ -15,6 +15,16 @@ header = """ // These symbols can be defined either in C/C++ or in Rust (using // `#[no_mangle]`). // +// Note that there are some `#define`s here which can be added before this +// header file is included to indicate how Wasmtime was built. This corresponds +// to the `wasmtime` crate's Cargo features where if the feature is disabled +// then the symbols will not be required. +// +// * `WASMTIME_SIGNALS_BASED_TRAPS` - corresponds to `signals-based-traps` +// // Some more information about this header can additionally be found at // . """ + +[defines] +'feature = signals-based-traps' = 'WASMTIME_SIGNALS_BASED_TRAPS' diff --git a/examples/min-platform/embedding/src/allocator.rs b/examples/min-platform/embedding/src/allocator.rs index 821119b0ffb1..38675cdf457f 100644 --- a/examples/min-platform/embedding/src/allocator.rs +++ b/examples/min-platform/embedding/src/allocator.rs @@ -63,24 +63,18 @@ unsafe impl GlobalAlloc for MyGlobalDmalloc { } } -// Hand-copied from `crates/wasmtime/src/runtime/vm/sys/custom/capi.rs`. -const PROT_READ: u32 = 1 << 0; -const PROT_WRITE: u32 = 1 << 1; -extern "C" { - fn wasmtime_mmap_new(size: usize, prot_flags: u32, ret: &mut *mut u8) -> i32; - fn wasmtime_page_size() -> usize; - fn wasmtime_munmap(ptr: *mut u8, size: usize) -> i32; -} +const INITIAL_HEAP_SIZE: usize = 64 * 1024; +static mut INITIAL_HEAP: [u8; INITIAL_HEAP_SIZE] = [0; INITIAL_HEAP_SIZE]; +static mut INITIAL_HEAP_ALLOCATED: bool = false; unsafe impl dlmalloc::Allocator for MyAllocator { - fn alloc(&self, size: usize) -> (*mut u8, usize, u32) { + fn alloc(&self, _size: usize) -> (*mut u8, usize, u32) { unsafe { - let mut ptr = ptr::null_mut(); - let rc = wasmtime_mmap_new(size, PROT_READ | PROT_WRITE, &mut ptr); - if rc != 0 { + if INITIAL_HEAP_ALLOCATED { (ptr::null_mut(), 0, 0) } else { - (ptr, size, 0) + INITIAL_HEAP_ALLOCATED = true; + (INITIAL_HEAP.as_mut_ptr(), INITIAL_HEAP_SIZE, 0) } } } @@ -93,11 +87,8 @@ unsafe impl dlmalloc::Allocator for MyAllocator { false } - fn free(&self, ptr: *mut u8, size: usize) -> bool { - unsafe { - wasmtime_munmap(ptr, size); - true - } + fn free(&self, _ptr: *mut u8, _size: usize) -> bool { + false } fn can_release_part(&self, _flags: u32) -> bool { @@ -109,7 +100,7 @@ unsafe impl dlmalloc::Allocator for MyAllocator { } fn page_size(&self) -> usize { - unsafe { wasmtime_page_size() } + 4096 } } diff --git a/examples/min-platform/embedding/src/lib.rs b/examples/min-platform/embedding/src/lib.rs index 49d0ecaf316f..f52b42e47b57 100644 --- a/examples/min-platform/embedding/src/lib.rs +++ b/examples/min-platform/embedding/src/lib.rs @@ -3,6 +3,7 @@ #[macro_use] extern crate alloc; +use alloc::string::ToString; use anyhow::Result; use wasmtime::{Engine, Instance, Linker, Module, Store}; @@ -53,14 +54,20 @@ fn run_result( fn smoke(module: &[u8]) -> Result<()> { let engine = Engine::default(); - let module = unsafe { Module::deserialize(&engine, module)? }; + let module = match deserialize(&engine, module)? { + Some(module) => module, + None => return Ok(()), + }; Instance::new(&mut Store::new(&engine, ()), &module, &[])?; Ok(()) } fn simple_add(module: &[u8]) -> Result<()> { let engine = Engine::default(); - let module = unsafe { Module::deserialize(&engine, module)? }; + let module = match deserialize(&engine, module)? { + Some(module) => module, + None => return Ok(()), + }; let mut store = Store::new(&engine, ()); let instance = Linker::new(&engine).instantiate(&mut store, &module)?; let func = instance.get_typed_func::<(u32, u32), u32>(&mut store, "add")?; @@ -70,7 +77,10 @@ fn simple_add(module: &[u8]) -> Result<()> { fn simple_host_fn(module: &[u8]) -> Result<()> { let engine = Engine::default(); - let module = unsafe { Module::deserialize(&engine, module)? }; + let module = match deserialize(&engine, module)? { + Some(module) => module, + None => return Ok(()), + }; let mut linker = Linker::<()>::new(&engine); linker.func_wrap("host", "multiply", |a: u32, b: u32| a.saturating_mul(b))?; let mut store = Store::new(&engine, ()); @@ -79,3 +89,24 @@ fn simple_host_fn(module: &[u8]) -> Result<()> { assert_eq!(func.call(&mut store, (2, 3, 4))?, 10); Ok(()) } + +fn deserialize(engine: &Engine, module: &[u8]) -> Result> { + match unsafe { Module::deserialize(engine, module) } { + Ok(module) => Ok(Some(module)), + Err(e) => { + // Currently if signals-based-traps are disabled then this example + // is expected to fail to load since loading native code requires + // virtual memory. In the future this will go away as when + // signals-based-traps is disabled then that means that the + // interpreter should be used which should work here. + if !cfg!(feature = "signals-based-traps") + && e.to_string() + .contains("requires virtual memory to be enabled") + { + Ok(None) + } else { + Err(e) + } + } + } +} diff --git a/examples/min-platform/embedding/wasmtime-platform.c b/examples/min-platform/embedding/wasmtime-platform.c index b02783597aae..c6e7c38c46eb 100644 --- a/examples/min-platform/embedding/wasmtime-platform.c +++ b/examples/min-platform/embedding/wasmtime-platform.c @@ -9,6 +9,8 @@ #include "wasmtime-platform.h" +#ifdef WASMTIME_SIGNALS_BASED_TRAPS + static int wasmtime_to_mmap_prot_flags(uint32_t prot_flags) { int flags = 0; if (prot_flags & WASMTIME_PROT_READ) @@ -53,6 +55,8 @@ int wasmtime_mprotect(uint8_t *ptr, uintptr_t size, uint32_t prot_flags) { uintptr_t wasmtime_page_size(void) { return sysconf(_SC_PAGESIZE); } +#endif // WASMTIME_SIGNALS_BASED_TRAPS + int32_t wasmtime_setjmp(const uint8_t **jmp_buf_out, void (*callback)(uint8_t *, uint8_t *), uint8_t *payload, uint8_t *callee) { @@ -68,6 +72,8 @@ void wasmtime_longjmp(const uint8_t *jmp_buf_ptr) { longjmp(*(jmp_buf *)jmp_buf_ptr, 1); } +#ifdef WASMTIME_SIGNALS_BASED_TRAPS + static wasmtime_trap_handler_t g_handler = NULL; static void handle_signal(int signo, siginfo_t *info, void *context) { @@ -134,6 +140,8 @@ void wasmtime_memory_image_free(struct wasmtime_memory_image *image) { abort(); } +#endif // WASMTIME_SIGNALS_BASED_TRAPS + // Pretend that this platform doesn't have threads where storing in a static is // ok. static uint8_t *WASMTIME_TLS = NULL; diff --git a/examples/min-platform/embedding/wasmtime-platform.h b/examples/min-platform/embedding/wasmtime-platform.h index 6583e40d9ece..a4996bdf9237 100644 --- a/examples/min-platform/embedding/wasmtime-platform.h +++ b/examples/min-platform/embedding/wasmtime-platform.h @@ -9,6 +9,13 @@ // These symbols can be defined either in C/C++ or in Rust (using // `#[no_mangle]`). // +// Note that there are some `#define`s here which can be added before this +// header file is included to indicate how Wasmtime was built. This corresponds +// to the `wasmtime` crate's Cargo features where if the feature is disabled +// then the symbols will not be required. +// +// * `WASMTIME_SIGNALS_BASED_TRAPS` - corresponds to `signals-based-traps` +// // Some more information about this header can additionally be found at // . @@ -23,27 +30,36 @@ #include #include +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Indicates that the memory region should be readable. */ #define WASMTIME_PROT_READ (1 << 0) +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Indicates that the memory region should be writable. */ #define WASMTIME_PROT_WRITE (1 << 1) +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Indicates that the memory region should be executable. */ #define WASMTIME_PROT_EXEC (1 << 2) +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Abstract pointer type used in the `wasmtime_memory_image_*` APIs which * is defined by the embedder. */ typedef struct wasmtime_memory_image wasmtime_memory_image; +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Handler function for traps in Wasmtime passed to `wasmtime_init_traps`. * @@ -74,11 +90,13 @@ typedef void (*wasmtime_trap_handler_t)(uintptr_t ip, uintptr_t fp, bool has_faulting_addr, uintptr_t faulting_addr); +#endif #ifdef __cplusplus extern "C" { #endif // __cplusplus +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Creates a new virtual memory mapping of the `size` specified with * protection bits specified in `prot_flags`. @@ -92,7 +110,9 @@ extern "C" { * Similar to `mmap(0, size, prot_flags, MAP_PRIVATE, 0, -1)` on Linux. */ extern int32_t wasmtime_mmap_new(uintptr_t size, uint32_t prot_flags, uint8_t **ret); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Remaps the virtual memory starting at `addr` going for `size` bytes to * the protections specified with a new blank mapping. @@ -106,7 +126,9 @@ extern int32_t wasmtime_mmap_new(uintptr_t size, uint32_t prot_flags, uint8_t ** * Similar to `mmap(addr, size, prot_flags, MAP_PRIVATE | MAP_FIXED, 0, -1)` on Linux. */ extern int32_t wasmtime_mmap_remap(uint8_t *addr, uintptr_t size, uint32_t prot_flags); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Unmaps memory at the specified `ptr` for `size` bytes. * @@ -118,7 +140,9 @@ extern int32_t wasmtime_mmap_remap(uint8_t *addr, uintptr_t size, uint32_t prot_ * Similar to `munmap` on Linux. */ extern int32_t wasmtime_munmap(uint8_t *ptr, uintptr_t size); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Configures the protections associated with a region of virtual memory * starting at `ptr` and going to `size`. @@ -128,11 +152,14 @@ extern int32_t wasmtime_munmap(uint8_t *ptr, uintptr_t size); * Similar to `mprotect` on Linux. */ extern int32_t wasmtime_mprotect(uint8_t *ptr, uintptr_t size, uint32_t prot_flags); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Returns the page size, in bytes, of the current system. */ extern uintptr_t wasmtime_page_size(void); +#endif /** * Used to setup a frame on the stack to longjmp back to in the future. @@ -168,6 +195,7 @@ extern int32_t wasmtime_setjmp(const uint8_t **jmp_buf, */ extern void wasmtime_longjmp(const uint8_t *jmp_buf); +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Initializes trap-handling logic for this platform. * @@ -183,7 +211,9 @@ extern void wasmtime_longjmp(const uint8_t *jmp_buf); * Returns 0 on success and an error code on failure. */ extern int32_t wasmtime_init_traps(wasmtime_trap_handler_t handler); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Attempts to create a new in-memory image of the `ptr`/`len` combo which * can be mapped to virtual addresses in the future. @@ -208,7 +238,9 @@ extern int32_t wasmtime_init_traps(wasmtime_trap_handler_t handler); extern int32_t wasmtime_memory_image_new(const uint8_t *ptr, uintptr_t len, struct wasmtime_memory_image **ret); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Maps the `image` provided to the virtual address at `addr` and `len`. * @@ -228,7 +260,9 @@ extern int32_t wasmtime_memory_image_new(const uint8_t *ptr, extern int32_t wasmtime_memory_image_map_at(struct wasmtime_memory_image *image, uint8_t *addr, uintptr_t len); +#endif +#if defined(WASMTIME_SIGNALS_BASED_TRAPS) /** * Deallocates the provided `wasmtime_memory_image`. * @@ -236,6 +270,7 @@ extern int32_t wasmtime_memory_image_map_at(struct wasmtime_memory_image *image, * deallocated and/or unmapped before this is called. */ extern void wasmtime_memory_image_free(struct wasmtime_memory_image *image); +#endif /** * Wasmtime requires a single pointer's space of TLS to be used at runtime, diff --git a/examples/min-platform/src/main.rs b/examples/min-platform/src/main.rs index 8fd6c063de4c..3aab4c4169f0 100644 --- a/examples/min-platform/src/main.rs +++ b/examples/min-platform/src/main.rs @@ -65,6 +65,17 @@ fn main() -> Result<()> { // Note that `Config::target` is used here to enable cross-compilation. let mut config = Config::new(); config.target(&triple)?; + + // If signals-based-traps are disabled then that additionally means that + // some configuration knobs need to be turned to match the expectations of + // the guest program being loaded. + if !cfg!(feature = "signals-based-traps") { + config.memory_reservation(0); + config.memory_guard_size(0); + config.memory_reservation_for_growth(0); + config.signals_based_traps(false); + } + let engine = Engine::new(&config)?; let smoke = engine.precompile_module(b"(module)")?; let simple_add = engine.precompile_module( diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 02ce3327199f..44bca5ff5612 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -10,7 +10,7 @@ use std::{ }, }; use wasmtime::component::Linker; -use wasmtime::{Config, Engine, Memory, MemoryType, Store, StoreLimits}; +use wasmtime::{Engine, Store, StoreLimits}; use wasmtime_wasi::{StreamError, StreamResult, WasiCtx, WasiCtxBuilder, WasiView}; use wasmtime_wasi_http::bindings::http::types::Scheme; use wasmtime_wasi_http::bindings::ProxyPre; @@ -639,18 +639,24 @@ impl wasmtime_wasi::Subscribe for LogStream { /// if it fails then the pooling allocator is not used and the normal mmap-based /// implementation is used instead. fn use_pooling_allocator_by_default() -> Result> { - const BITS_TO_TEST: u32 = 42; - let mut config = Config::new(); - config.wasm_memory64(true); - config.memory_reservation(1 << BITS_TO_TEST); - let engine = Engine::new(&config)?; - let mut store = Store::new(&engine, ()); - // NB: the maximum size is in wasm pages to take out the 16-bits of wasm - // page size here from the maximum size. - let ty = MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16))); - if Memory::new(&mut store, ty).is_ok() { - Ok(Some(true)) - } else { - Ok(None) + #[cfg(feature = "signals-based-traps")] + { + use wasmtime::{Config, Memory, MemoryType}; + const BITS_TO_TEST: u32 = 42; + let mut config = Config::new(); + config.wasm_memory64(true); + config.memory_reservation(1 << BITS_TO_TEST); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + // NB: the maximum size is in wasm pages to take out the 16-bits of wasm + // page size here from the maximum size. + let ty = MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16))); + if Memory::new(&mut store, ty).is_ok() { + Ok(Some(true)) + } else { + Ok(None) + } } + #[cfg(not(feature = "signals-based-traps"))] + return Ok(Some(false)); } diff --git a/tests/wast.rs b/tests/wast.rs index e7ff317849a7..a016afff1f71 100644 --- a/tests/wast.rs +++ b/tests/wast.rs @@ -4,7 +4,6 @@ use std::sync::{Condvar, LazyLock, Mutex}; use wasmtime::{ Config, Engine, InstanceAllocationStrategy, MpkEnabled, PoolingAllocationConfig, Store, }; -use wasmtime_environ::Memory; use wasmtime_wast::{SpectestConfig, WastContext}; use wasmtime_wast_util::{limits, Collector, Compiler, WastConfig, WastTest}; @@ -194,11 +193,14 @@ fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> { // Don't use 4gb address space reservations when not hogging memory, and // also don't reserve lots of memory after dynamic memories for growth // (makes growth slower). - cfg.memory_reservation(2 * u64::from(Memory::DEFAULT_PAGE_SIZE)); - cfg.memory_reservation_for_growth(0); + #[cfg(feature = "signals-based-traps")] + { + cfg.memory_reservation(2 * u64::from(wasmtime_environ::Memory::DEFAULT_PAGE_SIZE)); + cfg.memory_reservation_for_growth(0); - let small_guard = 64 * 1024; - cfg.memory_guard_size(small_guard); + let small_guard = 64 * 1024; + cfg.memory_guard_size(small_guard); + } } let _pooling_lock = if config.pooling { @@ -209,6 +211,10 @@ fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> { return Ok(()); } + if !cfg!(feature = "signals-based-traps") { + return Ok(()); + } + // Reduce the virtual memory required to run multi-memory-based tests. // // The configuration parameters below require that a bare minimum @@ -220,6 +226,7 @@ fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> { // force the usage of static memories without guards to reduce the VM // impact. let max_memory_size = limits::MEMORY_SIZE; + #[cfg(feature = "signals-based-traps")] if multi_memory { cfg.memory_reservation(max_memory_size as u64); cfg.memory_reservation_for_growth(0);