Skip to content

Commit

Permalink
Use signals-based-traps to gate virtual memory too
Browse files Browse the repository at this point in the history
This commit updates the `signals-based-traps` Cargo feature to
additionally gate the use of virtual memory in Wasmtime. When this
feature is disabled it now additionally disables the reliance on virtual
memory as an implementation detail of WebAssembly linear memories.
Additionally `CodeMemory` no longer can rely on virtual memory meaning
that code cannot be made executable.

The main purpose of this commit is to provide a build mode of Wasmtime
that does not rely on virtual memory at all. This is currently only
suitable for Pulley where there is no new native code but it should be
an easier starting point for other small adjustments in the future if
necessary.

prtest:full
  • Loading branch information
alexcrichton committed Nov 16, 2024
1 parent 1b2d866 commit aab4dcd
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 49 deletions.
6 changes: 6 additions & 0 deletions crates/environ/src/obj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 13 additions & 4 deletions crates/wasmtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,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
Expand Down Expand Up @@ -236,7 +240,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",
Expand All @@ -263,7 +266,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"]
Expand Down Expand Up @@ -325,4 +332,6 @@ reexport-wasmparser = []
# 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 = []
signals-based-traps = [
"dep:wasmtime-jit-icache-coherence",
]
52 changes: 37 additions & 15 deletions crates/wasmtime/src/runtime/code_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
///
Expand All @@ -20,6 +19,7 @@ pub struct CodeMemory {
debug_registration: Option<crate::runtime::vm::GdbJitImageRegistration>,
published: bool,
enable_branch_protection: bool,
needs_executable: bool,
#[cfg(feature = "debug-builtins")]
has_native_debug_info: bool,

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
21 changes: 18 additions & 3 deletions crates/wasmtime/src/runtime/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,7 +67,6 @@ pub use crate::runtime::vm::instance::{
pub use crate::runtime::vm::memory::{
Memory, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory,
};
pub use crate::runtime::vm::mmap::Mmap;
pub use crate::runtime::vm::mmap_vec::MmapVec;
pub use crate::runtime::vm::mpk::MpkEnabled;
pub use crate::runtime::vm::store_box::*;
Expand All @@ -86,8 +84,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.
Expand Down Expand Up @@ -337,6 +348,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);

Expand All @@ -352,13 +364,15 @@ 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
}

/// 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<u64> {
let page_size = u64::try_from(crate::runtime::vm::host_page_size()).err2anyhow()?;
debug_assert!(page_size.is_power_of_two());
Expand All @@ -371,6 +385,7 @@ pub fn round_u64_up_to_host_pages(bytes: u64) -> Result<u64> {
}

/// 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<usize> {
let bytes = u64::try_from(bytes).err2anyhow()?;
let rounded = round_u64_up_to_host_pages(bytes)?;
Expand Down
46 changes: 46 additions & 0 deletions crates/wasmtime/src/runtime/vm/cow_disabled.rs
Original file line number Diff line number Diff line change
@@ -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<MemoryImage>> {
None
}
}

#[derive(Debug, PartialEq)]
pub enum MemoryImage {}

impl ModuleMemoryImages {
pub fn new(
_module: &Module,
_wasm_data: &[u8],
_mmap: Option<&MmapVec>,
) -> Result<Option<ModuleMemoryImages>> {
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 {}
}
}
13 changes: 10 additions & 3 deletions crates/wasmtime/src/runtime/vm/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ use core::ops::Range;
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;
Expand Down Expand Up @@ -125,11 +127,13 @@ impl RuntimeMemoryCreator for DefaultMemoryCreator {
minimum: usize,
maximum: Option<usize>,
) -> Result<Box<dyn RuntimeLinearMemory>> {
#[cfg(feature = "signals-based-traps")]
if tunables.signals_based_traps || tunables.memory_guard_size > 0 {
Ok(Box::new(MmapMemory::new(ty, tunables, minimum, maximum)?))
} else {
Ok(Box::new(MallocMemory::new(ty, tunables, minimum)?))
return Ok(Box::new(MmapMemory::new(ty, tunables, minimum, maximum)?));
}

let _ = maximum;
Ok(Box::new(MallocMemory::new(ty, tunables, minimum)?))
}
}

Expand Down Expand Up @@ -471,6 +475,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(),
Expand All @@ -484,6 +489,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 {
Expand Down
5 changes: 0 additions & 5 deletions crates/wasmtime/src/runtime/vm/mmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit aab4dcd

Please sign in to comment.