Skip to content

Commit 84852f7

Browse files
authored
Don't use MemoryStyle for heap base pointer relocations (bytecodealliance#9569)
* Don't use `MemoryStyle` for heap base pointer relocations Instead add a helper method to `Memory` which indicates whether the base pointer of memory can move. Use this and plumb the result around to the various locations that require it. This improves the `readonly` application of the base pointer in Cranelift by having the optimization kick in in more scenarios. It additionally fixes combining shared linear memories with 64-bit addressing or a page size of 1 by adding a runtime check before relocation of a linear memory that it's allowed to do so. A few codegen tests are added to ensure that `readonly` is applied where it wasn't previously and additionally a new `*.wast` test was added with the cross product of various features of linear memories and some basic tests to ensure that the memories all work as expected. This refactoring fixes two preexisting issues about `shared` memories requiring a "static" memory style. The restriction is now based on whether the pointer can relocate or not and that's upheld via the new trait method added here. To account for these bug fixes the fuzzers have been updated to allow mixing the `threads` proposal with `memory64` or `custom-page-sizes`. Closes bytecodealliance#4267 Closes bytecodealliance#9523 * Optionally increase the allocation size for dynamic memories This code will be short-lived due to scheduled future refactorings but the idea is that when a "dynamic" memory is chosen the minimum size of the allocation needs to be at least `tunables.memory_reservation` to fit the constraints of the rest of the system, so be sure to factor that in.
1 parent ccb3396 commit 84852f7

File tree

13 files changed

+517
-46
lines changed

13 files changed

+517
-46
lines changed

crates/cranelift/src/func_environ.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,7 +2371,7 @@ impl<'module_environment> crate::translate::FuncEnvironment
23712371
// If we have a declared maximum, we can make this a "static" heap, which is
23722372
// allocated up front and never moved.
23732373
let style = MemoryStyle::for_memory(memory, self.tunables);
2374-
let (readonly_base, base_fact, memory_type) = match style {
2374+
let (base_fact, memory_type) = match style {
23752375
MemoryStyle::Dynamic { .. } => {
23762376
let (base_fact, data_mt) = if let Some(ptr_memtype) = ptr_memtype {
23772377
// Create a memtype representing the untyped memory region.
@@ -2428,7 +2428,7 @@ impl<'module_environment> crate::translate::FuncEnvironment
24282428
(None, None)
24292429
};
24302430

2431-
(false, base_fact, data_mt)
2431+
(base_fact, data_mt)
24322432
}
24332433
MemoryStyle::Static {
24342434
byte_reservation: bound_bytes,
@@ -2476,12 +2476,12 @@ impl<'module_environment> crate::translate::FuncEnvironment
24762476
} else {
24772477
(None, None)
24782478
};
2479-
(true, base_fact, data_mt)
2479+
(base_fact, data_mt)
24802480
}
24812481
};
24822482

24832483
let mut flags = MemFlags::trusted().with_checked();
2484-
if readonly_base {
2484+
if !memory.memory_may_move(self.tunables) {
24852485
flags.set_readonly();
24862486
}
24872487
let heap_base = func.create_global_value(ir::GlobalValueData::Load {

crates/environ/src/types.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{wasm_unsupported, WasmResult};
1+
use crate::{wasm_unsupported, Tunables, WasmResult};
22
use alloc::borrow::Cow;
33
use alloc::boxed::Box;
44
use core::{fmt, ops::Range};
@@ -1810,6 +1810,32 @@ impl Memory {
18101810
None
18111811
}
18121812
}
1813+
1814+
/// Returs whether or not the base pointer of this memory is allowed to be
1815+
/// relocated at runtime.
1816+
///
1817+
/// When this function returns `false` then it means that after the initial
1818+
/// allocation the base pointer is constant for the entire lifetime of a
1819+
/// memory. This can enable compiler optimizations, for example.
1820+
pub fn memory_may_move(&self, tunables: &Tunables) -> bool {
1821+
// Shared memories cannot ever relocate their base pointer so the
1822+
// settings configured in the engine must be appropriate for them ahead
1823+
// of time.
1824+
if self.shared {
1825+
return false;
1826+
}
1827+
1828+
// If movement is disallowed in engine configuration, then the answer is
1829+
// "no".
1830+
if !tunables.memory_may_move {
1831+
return false;
1832+
}
1833+
1834+
// If the maximum size of this memory is above the threshold of the
1835+
// initial memory reservation then the memory may move.
1836+
let max = self.maximum_byte_size().unwrap_or(u64::MAX);
1837+
max > tunables.memory_reservation
1838+
}
18131839
}
18141840

18151841
#[derive(Copy, Clone, Debug)]

crates/fuzzing/src/generators/config.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -496,15 +496,20 @@ impl WasmtimeConfig {
496496
self.debug_info = false;
497497
}
498498

499+
// Forcibly don't use the `CustomUnaligned` memory configuration when
500+
// wasm threads are enabled or when the pooling allocator is used. For
501+
// the pooling allocator it doesn't use custom memory creators anyway
502+
// and for wasm threads that will require some refactoring of the
503+
// `LinearMemory` trait to bubble up the request that the linear memory
504+
// not move. Otherwise that just generates a panic right now.
505+
if config.threads_enabled || matches!(self.strategy, InstanceAllocationStrategy::Pooling(_))
506+
{
507+
self.avoid_custom_unaligned_memory(u)?;
508+
}
509+
499510
// If using the pooling allocator, constrain the memory and module configurations
500511
// to the module limits.
501512
if let InstanceAllocationStrategy::Pooling(pooling) = &mut self.strategy {
502-
// Forcibly don't use the `CustomUnaligned` memory configuration
503-
// with the pooling allocator active.
504-
if let MemoryConfig::CustomUnaligned = self.memory_config {
505-
self.memory_config = MemoryConfig::Normal(u.arbitrary()?);
506-
}
507-
508513
// If the pooling allocator is used, do not allow shared memory to
509514
// be created. FIXME: see
510515
// https://github.com/bytecodealliance/wasmtime/issues/4244.
@@ -578,6 +583,15 @@ impl WasmtimeConfig {
578583
Ok(())
579584
}
580585

586+
/// Helper to switch `MemoryConfig::CustomUnaligned` to
587+
/// `MemoryConfig::Normal`
588+
fn avoid_custom_unaligned_memory(&mut self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
589+
if let MemoryConfig::CustomUnaligned = self.memory_config {
590+
self.memory_config = MemoryConfig::Normal(u.arbitrary()?);
591+
}
592+
Ok(())
593+
}
594+
581595
/// Helper method to handle some dependencies between various configuration
582596
/// options. This is intended to be called whenever a `Config` is created or
583597
/// modified to ensure that the final result is an instantiable `Config`.

crates/fuzzing/src/generators/module.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ impl<'a> Arbitrary<'a> for ModuleConfig {
3939
config.custom_page_sizes_enabled = u.arbitrary()?;
4040
config.wide_arithmetic_enabled = u.arbitrary()?;
4141
config.memory64_enabled = u.ratio(1, 20)?;
42-
// Allow the threads proposal if memory64 is not already enabled. FIXME:
43-
// to allow threads and memory64 to coexist, see
44-
// https://github.com/bytecodealliance/wasmtime/issues/4267.
45-
config.threads_enabled = !config.memory64_enabled && u.ratio(1, 20)?;
42+
config.threads_enabled = u.ratio(1, 20)?;
4643
// Allow multi-memory but make it unlikely
4744
if u.ratio(1, 20)? {
4845
config.max_memories = config.max_memories.max(2);
@@ -56,12 +53,6 @@ impl<'a> Arbitrary<'a> for ModuleConfig {
5653
// do that most of the time.
5754
config.disallow_traps = u.ratio(9, 10)?;
5855

59-
// FIXME(#9523) this `if` should ideally be deleted, requires some
60-
// refactoring internally.
61-
if config.custom_page_sizes_enabled && config.threads_enabled {
62-
config.custom_page_sizes_enabled = false;
63-
}
64-
6556
Ok(ModuleConfig { config })
6657
}
6758
}

crates/wasmtime/src/runtime/trampoline/memory.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ impl RuntimeLinearMemory for LinearMemoryProxy {
114114
fn wasm_accessible(&self) -> Range<usize> {
115115
self.mem.wasm_accessible()
116116
}
117+
118+
fn memory_may_move(&self) -> bool {
119+
// FIXME(#9568): should propagate this up to the `LinearMemory` trait,
120+
// but for now pessimistically assume that consumers might move linear
121+
// memory.
122+
true
123+
}
117124
}
118125

119126
#[derive(Clone)]

crates/wasmtime/src/runtime/vm/memory.rs

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub trait RuntimeLinearMemory: Send + Sync {
7171
/// Returns `None` if the memory is unbounded.
7272
fn maximum_byte_size(&self) -> Option<usize>;
7373

74+
/// Returns whether the base pointer of this memory may be relocated over
75+
/// time to accommodate requests for growth.
76+
fn memory_may_move(&self) -> bool;
77+
7478
/// Grows a memory by `delta_pages`.
7579
///
7680
/// This performs the necessary checks on the growth before delegating to
@@ -203,6 +207,9 @@ pub struct MmapMemory {
203207
// specified so that the cost of repeated growth is amortized.
204208
extra_to_reserve_on_growth: usize,
205209

210+
// Whether or not this memory is allowed to relocate the base pointer.
211+
memory_may_move: bool,
212+
206213
// Size in bytes of extra guard pages before the start and after the end to
207214
// optimize loads and stores with constant offsets.
208215
pre_guard_size: usize,
@@ -240,12 +247,23 @@ impl MmapMemory {
240247
let pre_guard_bytes = round_usize_up_to_host_pages(pre_guard_bytes)?;
241248

242249
let (alloc_bytes, extra_to_reserve_on_growth) = match style {
243-
// Dynamic memories start with the minimum size plus the `reserve`
244-
// amount specified to grow into.
245-
MemoryStyle::Dynamic { reserve } => (
246-
round_usize_up_to_host_pages(minimum)?,
247-
round_usize_up_to_host_pages(usize::try_from(reserve).unwrap())?,
248-
),
250+
// Dynamic memories start with the larger of the minimum size of
251+
// memory or the configured memory reservation. This ensures that
252+
// the allocation fits the constraints in `tunables` where it must
253+
// be as large as the specified reservation.
254+
//
255+
// Then `reserve` amount is added extra to the virtual memory
256+
// allocation for memory to grow into.
257+
MemoryStyle::Dynamic { reserve } => {
258+
let minimum = round_usize_up_to_host_pages(minimum)?;
259+
let reservation = usize::try_from(tunables.memory_reservation).unwrap();
260+
let reservation = round_usize_up_to_host_pages(reservation)?;
261+
262+
(
263+
minimum.max(reservation),
264+
round_usize_up_to_host_pages(usize::try_from(reserve).unwrap())?,
265+
)
266+
}
249267

250268
// Static memories will never move in memory and consequently get
251269
// their entire allocation up-front with no extra room to grow into.
@@ -305,6 +323,7 @@ impl MmapMemory {
305323
offset_guard_size: offset_guard_bytes,
306324
extra_to_reserve_on_growth,
307325
memory_image,
326+
memory_may_move: ty.memory_may_move(tunables),
308327
})
309328
}
310329

@@ -332,13 +351,21 @@ impl RuntimeLinearMemory for MmapMemory {
332351
self.maximum
333352
}
334353

354+
fn memory_may_move(&self) -> bool {
355+
self.memory_may_move
356+
}
357+
335358
fn grow_to(&mut self, new_size: usize) -> Result<()> {
336359
assert!(usize_is_multiple_of_host_page_size(self.offset_guard_size));
337360
assert!(usize_is_multiple_of_host_page_size(self.pre_guard_size));
338361
assert!(usize_is_multiple_of_host_page_size(self.mmap.len()));
339362

340363
let new_accessible = round_usize_up_to_host_pages(new_size)?;
341364
if new_accessible > self.mmap.len() - self.offset_guard_size - self.pre_guard_size {
365+
if !self.memory_may_move {
366+
bail!("disallowing growth as base pointer of memory cannot move");
367+
}
368+
342369
// If the new size of this heap exceeds the current size of the
343370
// allocation we have, then this must be a dynamic heap. Use
344371
// `new_size` to calculate a new size of an allocation, allocate it,
@@ -507,6 +534,10 @@ impl RuntimeLinearMemory for StaticMemory {
507534
Some(self.capacity)
508535
}
509536

537+
fn memory_may_move(&self) -> bool {
538+
false
539+
}
540+
510541
fn grow_to(&mut self, new_byte_size: usize) -> Result<()> {
511542
// Never exceed the static memory size; this check should have been made
512543
// prior to arriving here.
@@ -556,10 +587,27 @@ impl Memory {
556587
let (minimum, maximum) = Self::limit_new(ty, Some(store))?;
557588
let allocation = creator.new_memory(ty, tunables, minimum, maximum, memory_image)?;
558589
let allocation = if ty.shared {
559-
Box::new(SharedMemory::wrap(ty, tunables, allocation)?)
590+
Box::new(SharedMemory::wrap(ty, allocation)?)
560591
} else {
561592
allocation
562593
};
594+
595+
// Double-check that the created memory respects the safety invariant of
596+
// whether the memory may move or not at runtime.
597+
//
598+
// * If the memory is allowed to move, that's ok.
599+
// * If the allocation doesn't allow the memory to move, that's ok.
600+
// * If the heap has a static size meaning the min is the same as the
601+
// max, that's ok since it'll never be requested to move.
602+
//
603+
// Otherwise something went wrong so trigger an assert.
604+
assert!(
605+
ty.memory_may_move(tunables)
606+
|| !allocation.memory_may_move()
607+
|| ty.static_heap_size().is_some(),
608+
"this linear memory should not be able to move its base pointer",
609+
);
610+
563611
Ok(Memory(allocation))
564612
}
565613

@@ -652,16 +700,11 @@ impl Memory {
652700
// informing the limiter is lossy and may not be 100% accurate, but for
653701
// now the expected uses of limiter means that's ok.
654702
if let Some(store) = store {
655-
// We ignore the store limits for shared memories since they are
656-
// technically not created within a store (though, trickily, they
657-
// may be associated with one in order to get a `vmctx`).
658-
if !ty.shared {
659-
if !store.memory_growing(0, minimum.unwrap_or(absolute_max), maximum)? {
660-
bail!(
661-
"memory minimum size of {} pages exceeds memory limits",
662-
ty.limits.min
663-
);
664-
}
703+
if !store.memory_growing(0, minimum.unwrap_or(absolute_max), maximum)? {
704+
bail!(
705+
"memory minimum size of {} pages exceeds memory limits",
706+
ty.limits.min
707+
);
665708
}
666709
}
667710

crates/wasmtime/src/runtime/vm/threads/shared_memory.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::ops::Range;
88
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
99
use std::sync::{Arc, RwLock};
1010
use std::time::{Duration, Instant};
11-
use wasmtime_environ::{MemoryStyle, Trap, Tunables};
11+
use wasmtime_environ::{Trap, Tunables};
1212

1313
/// For shared memory (and only for shared memory), this lock-version restricts
1414
/// access when growing the memory or checking its size. This is to conform with
@@ -33,21 +33,19 @@ impl SharedMemory {
3333
pub fn new(ty: &wasmtime_environ::Memory, tunables: &Tunables) -> Result<Self> {
3434
let (minimum_bytes, maximum_bytes) = Memory::limit_new(ty, None)?;
3535
let mmap_memory = MmapMemory::new(ty, tunables, minimum_bytes, maximum_bytes, None)?;
36-
Self::wrap(ty, tunables, Box::new(mmap_memory))
36+
Self::wrap(ty, Box::new(mmap_memory))
3737
}
3838

3939
/// Wrap an existing [Memory] with the locking provided by a [SharedMemory].
4040
pub fn wrap(
4141
ty: &wasmtime_environ::Memory,
42-
tunables: &Tunables,
4342
mut memory: Box<dyn RuntimeLinearMemory>,
4443
) -> Result<Self> {
4544
if !ty.shared {
4645
bail!("shared memory must have a `shared` memory type");
4746
}
48-
let style = MemoryStyle::for_memory(*ty, tunables);
49-
if !matches!(style, MemoryStyle::Static { .. }) {
50-
bail!("shared memory can only be built from a static memory allocation")
47+
if memory.memory_may_move() {
48+
bail!("shared memory cannot use a memory which may relocate the base pointer")
5149
}
5250
assert!(
5351
memory.as_any_mut().type_id() != std::any::TypeId::of::<SharedMemory>(),
@@ -233,4 +231,9 @@ impl RuntimeLinearMemory for SharedMemory {
233231
fn wasm_accessible(&self) -> Range<usize> {
234232
self.0.memory.read().unwrap().wasm_accessible()
235233
}
234+
235+
fn memory_may_move(&self) -> bool {
236+
debug_assert!(!self.0.memory.read().unwrap().memory_may_move());
237+
false
238+
}
236239
}

crates/wasmtime/src/runtime/vm/threads/shared_memory_disabled.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ pub enum SharedMemory {}
1212
impl SharedMemory {
1313
pub fn wrap(
1414
_ty: &wasmtime_environ::Memory,
15-
_tunables: &Tunables,
1615
_memory: Box<dyn RuntimeLinearMemory>,
1716
) -> Result<Self> {
1817
bail!("support for shared memories was disabled at compile time");
@@ -101,4 +100,8 @@ impl RuntimeLinearMemory for SharedMemory {
101100
fn wasm_accessible(&self) -> Range<usize> {
102101
match *self {}
103102
}
103+
104+
fn memory_may_move(&self) -> bool {
105+
match *self {}
106+
}
104107
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
;;! test = "optimize"
2+
;;! target = "x86_64"
3+
;;! flags = ["-Omemory-reservation=0x20000", "-Omemory-may-move=n"]
4+
5+
(module
6+
(memory 1)
7+
(func $load (param i32) (result i32)
8+
(i32.load (local.get 0)))
9+
)
10+
;; function u0:0(i64 vmctx, i64, i32) -> i32 tail {
11+
;; gv0 = vmctx
12+
;; gv1 = load.i64 notrap aligned readonly gv0+8
13+
;; gv2 = load.i64 notrap aligned gv1
14+
;; gv3 = vmctx
15+
;; gv4 = load.i64 notrap aligned gv3+104
16+
;; gv5 = load.i64 notrap aligned readonly checked gv3+96
17+
;; stack_limit = gv2
18+
;;
19+
;; block0(v0: i64, v1: i64, v2: i32):
20+
;; @0020 v4 = uextend.i64 v2
21+
;; @0020 v5 = iconst.i64 0x0001_fffc
22+
;; @0020 v6 = icmp ugt v4, v5 ; v5 = 0x0001_fffc
23+
;; @0020 v9 = iconst.i64 0
24+
;; @0020 v7 = load.i64 notrap aligned readonly checked v0+96
25+
;; @0020 v8 = iadd v7, v4
26+
;; @0020 v10 = select_spectre_guard v6, v9, v8 ; v9 = 0
27+
;; @0020 v11 = load.i32 little heap v10
28+
;; @0023 jump block1
29+
;;
30+
;; block1:
31+
;; @0023 return v11
32+
;; }

0 commit comments

Comments
 (0)