Skip to content

Commit b860c2c

Browse files
authored
Adjust behavior of 4gb memories with custom page sizes (#12884)
* Adjust behavior of 4gb memories with custom page sizes This commit adjust what happens when a linear memory grows up to 4gb large when custom page sizes are used. This is an open question in the upstream proposal at WebAssembly/custom-page-sizes#45 but without any special handling a return value of -1 is ambiguous if it succeeded or failed. For now eagerly trap memory operations reaching these conditions while the upstream specification question is resolved. * Fix CI * Debug CI failure prtest:full * Fix 32-bit platforms
1 parent 0e1b633 commit b860c2c

9 files changed

Lines changed: 139 additions & 24 deletions

File tree

crates/environ/src/types.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,6 +2320,36 @@ impl Memory {
23202320
let max = self.maximum_byte_size().unwrap_or(u64::MAX);
23212321
max > tunables.memory_reservation
23222322
}
2323+
2324+
/// Tests whether this memory type is allowed to grow up to `size` bytes.
2325+
///
2326+
/// This is only applicable to custom-page-size memories which have a page
2327+
/// size of a single byte. In that situation growth beyond `-1i32 as u32`
2328+
/// bytes is not allowed because at that point memory growth succeeding and
2329+
/// failing would be indistinguishable in the return value of `memory.grow`,
2330+
/// for example. To handle this 32-bit memories are only allowed to grow to
2331+
/// `-2i32 as u32`, for example, and 64-bit memories with a page size of 1
2332+
/// are allowed to grow up to the maximum size.
2333+
pub fn allow_growth_to(&self, size: usize) -> bool {
2334+
if self.page_size_log2 != 0 {
2335+
return true;
2336+
}
2337+
match self.idx_type {
2338+
// For a 32-bit memory using 1-byte pages the last 2 bytes of the
2339+
// 32-bit address space are addressable but disallowed for now. A
2340+
// memory that is 4GiB in size cannot report its size via
2341+
// `memory.size`, and a memory that is 4GiB-1 bytes in size cannot
2342+
// be distinguished when 1 byte is added from an allocation
2343+
// failure. To handle this the memory is capped at 4GiB-2 which
2344+
// means that all memory-related instructions and such will have
2345+
// unambiguous return codes.
2346+
IndexType::I32 => size < 0xffff_ffff,
2347+
2348+
// Assume that for a 64-bit memory using 1-byte pages it's going to
2349+
// exhaust system resources before a limit is actually reached.
2350+
IndexType::I64 => true,
2351+
}
2352+
}
23232353
}
23242354

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

crates/test-util/src/wast.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,11 @@ impl WastTest {
457457

458458
// Some tests are known to fail with the pooling allocator
459459
if config.pooling {
460+
// allocates too much memory for the pooling configuration here
461+
if self.config.hogs_memory() {
462+
return true;
463+
}
460464
let unsupported = [
461-
// allocates too much memory for the pooling configuration here
462-
"misc_testsuite/memory64/more-than-4gb.wast",
463465
// shared memories + pooling allocator aren't supported yet
464466
"misc_testsuite/memory-combos.wast",
465467
"misc_testsuite/threads/atomics-end-of-memory.wast",

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

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -360,15 +360,19 @@ impl Memory {
360360
)
361361
})?;
362362

363+
if !ty.allow_growth_to(minimum) {
364+
bail!(
365+
"memory minimum size of {} pages exceeds memory limits",
366+
ty.limits.min
367+
);
368+
}
369+
363370
Ok((minimum, maximum))
364371
}
365372

366373
/// Returns this memory's page size, in bytes.
367374
pub fn page_size(&self) -> u64 {
368-
match self {
369-
Memory::Local(mem) => mem.page_size(),
370-
Memory::Shared(mem) => mem.page_size(),
371-
}
375+
self.ty().page_size()
372376
}
373377

374378
/// Returns the size of this memory, in bytes.
@@ -411,6 +415,39 @@ impl Memory {
411415
delta_pages: u64,
412416
limiter: Option<&mut StoreResourceLimiter<'_>>,
413417
) -> Result<Option<usize>, Error> {
418+
let new_size = delta_pages
419+
.checked_mul(self.page_size())
420+
.and_then(|new_bytes| {
421+
let new_bytes = usize::try_from(new_bytes).ok()?;
422+
self.byte_size().checked_add(new_bytes)
423+
});
424+
match new_size {
425+
Some(new_size) => {
426+
// FIXME(WebAssembly/custom-page-sizes#45) - what should the
427+
// behavior here be exactly? A trap? Return -1? A smaller limit?
428+
// Unclear!
429+
//
430+
// Trap for now to make this as noisy/conservative as possible.
431+
if !self.ty().allow_growth_to(new_size) {
432+
bail!(
433+
"disallowing growth to {new_size:#x} bytes based on \
434+
page size"
435+
)
436+
}
437+
}
438+
439+
// If the new size in memory isn't representable in a `usize` then
440+
// there's no need to actually try to grow it to that size. It's
441+
// impossible to succeed so just fail it early.
442+
None => {
443+
if let Some(limiter) = limiter {
444+
let err = crate::format_err!("memory growth exceeds address space");
445+
limiter.memory_grow_failed(err)?;
446+
}
447+
return Ok(None);
448+
}
449+
}
450+
414451
let result = match self {
415452
Memory::Local(mem) => mem.grow(delta_pages, limiter).await?,
416453
Memory::Shared(mem) => mem.grow(delta_pages)?,
@@ -513,6 +550,13 @@ impl Memory {
513550
Memory::Shared(mem) => mem.wasm_accessible(),
514551
}
515552
}
553+
554+
fn ty(&self) -> &wasmtime_environ::Memory {
555+
match self {
556+
Memory::Local(mem) => mem.ty(),
557+
Memory::Shared(mem) => mem.ty(),
558+
}
559+
}
516560
}
517561

518562
/// An owned allocation of a wasm linear memory.
@@ -579,8 +623,8 @@ impl LocalMemory {
579623
})
580624
}
581625

582-
pub fn page_size(&self) -> u64 {
583-
self.ty.page_size()
626+
pub fn ty(&self) -> &wasmtime_environ::Memory {
627+
&self.ty
584628
}
585629

586630
/// Grows a memory by `delta_pages`.
@@ -601,7 +645,7 @@ impl LocalMemory {
601645
return Ok(Some((old_byte_size, old_byte_size)));
602646
}
603647

604-
let page_size = usize::try_from(self.page_size()).unwrap();
648+
let page_size = usize::try_from(self.ty().page_size()).unwrap();
605649

606650
// The largest wasm-page-aligned region of memory is possible to
607651
// represent in a `usize`. This will be impossible for the system to

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ impl SharedMemory {
6767
}
6868

6969
/// Return the memory type for this [`SharedMemory`].
70-
pub fn ty(&self) -> wasmtime_environ::Memory {
71-
self.0.ty
70+
pub fn ty(&self) -> &wasmtime_environ::Memory {
71+
&self.0.ty
7272
}
7373

7474
/// Convert this shared memory into a [`Memory`].
@@ -172,10 +172,6 @@ impl SharedMemory {
172172
})
173173
}
174174

175-
pub(crate) fn page_size(&self) -> u64 {
176-
self.0.ty.page_size()
177-
}
178-
179175
pub(crate) fn byte_size(&self) -> usize {
180176
self.0.memory.read().unwrap().byte_size()
181177
}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ impl SharedMemory {
1515
bail!("support for shared memories was disabled at compile time");
1616
}
1717

18-
pub fn ty(&self) -> wasmtime_environ::Memory {
18+
pub fn ty(&self) -> &wasmtime_environ::Memory {
1919
match *self {}
2020
}
2121

@@ -53,10 +53,6 @@ impl SharedMemory {
5353
match *self {}
5454
}
5555

56-
pub(crate) fn page_size(&self) -> u64 {
57-
match *self {}
58-
}
59-
6056
pub(crate) fn byte_size(&self) -> usize {
6157
match *self {}
6258
}

tests/all/cli_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,12 +876,12 @@ fn memory_growth_failure() -> Result<()> {
876876
"tests/all/cli_tests/memory-grow-failure.wat",
877877
])
878878
.output()?;
879-
assert!(!output.status.success());
880879
let stderr = String::from_utf8_lossy(&output.stderr);
881880
assert!(
882881
stderr.contains("forcing a memory growth failure to be a trap"),
883882
"bad stderr: {stderr}"
884883
);
884+
assert!(!output.status.success());
885885
Ok(())
886886
}
887887

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
;;! hogs_memory = true
2+
3+
(module
4+
(memory 0xffff)
5+
6+
(func (export "grow") (param i32) (result i32)
7+
local.get 0
8+
memory.grow)
9+
)
10+
11+
(assert_return (invoke "grow" (i32.const 0)) (i32.const 0xffff))
12+
(assert_return (invoke "grow" (i32.const 1)) (i32.const 0xffff))
13+
(assert_return (invoke "grow" (i32.const 0)) (i32.const 0x10000))
14+
(assert_return (invoke "grow" (i32.const 1)) (i32.const -1))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
;;! custom_page_sizes = true
2+
;;! hogs_memory = true
3+
4+
;; FIXME(WebAssembly/custom-page-sizes#45): unclear what the semantics of this
5+
;; test should be. For now Wasmtime traps so this tests that traps happen, but
6+
;; this may change in the specification itself. Either way the problem here is
7+
;; that with 1-byte pages an allocation of 0xffff_ffff bytes is
8+
;; indistinguishable from a growth failure which returns -1. Somehow this needs
9+
;; reconciliation and for now it's done as a trap.
10+
11+
(assert_trap
12+
(module
13+
(memory 0xffff_ffff (pagesize 1))
14+
)
15+
"memory minimum size of 4294967295 pages exceeds memory limits")
16+
17+
(module $m
18+
(memory (export "memory") 0xffff_fffe (pagesize 1))
19+
)
20+
21+
(module
22+
(import "m" "memory" (memory 0 (pagesize 1)))
23+
24+
(func (export "grow") (param i32) (result i32)
25+
local.get 0
26+
memory.grow)
27+
)
28+
29+
(assert_trap (invoke "grow" (i32.const 1)) "disallowing growth to 0xffffffff bytes based on page size")
30+
(assert_trap (invoke "grow" (i32.const 2)) "disallowing growth to 0x100000000 bytes based on page size")
31+
(assert_trap (invoke "grow" (i32.const 100)) "disallowing growth to 0x100000062 bytes based on page size")
32+
(assert_trap (invoke "grow" (i32.const -1)) "disallowing growth to 0x1fffffffd bytes based on page size")
33+
(assert_return (invoke "grow" (i32.const 0)) (i32.const -2))

tests/misc_testsuite/memory-combos.wast

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
(assert_trap (invoke "store_m3" (i32.const -1) (i32.const 0)) "out of bounds memory access")
128128
(assert_return (invoke "grow_m3" (i32.const 1)) (i32.const 1))
129129
(assert_return (invoke "size_m3") (i32.const 2))
130-
(assert_return (invoke "grow_m3" (i32.const -1)) (i32.const -1))
130+
(assert_trap (invoke "grow_m3" (i32.const -1)) "disallowing growth")
131131
(assert_return (invoke "load_m3" (i32.const 1)) (i32.const 0))
132132
(assert_return (invoke "store_m3" (i32.const 1) (i32.const 1)))
133133
(assert_return (invoke "load_m3" (i32.const 1)) (i32.const 1))
@@ -144,7 +144,7 @@
144144
(assert_trap (invoke "store_m4" (i32.const -1) (i32.const 0)) "out of bounds memory access")
145145
(assert_return (invoke "grow_m4" (i32.const 1)) (i32.const 1))
146146
(assert_return (invoke "size_m4") (i32.const 2))
147-
(assert_return (invoke "grow_m4" (i32.const -1)) (i32.const -1))
147+
(assert_trap (invoke "grow_m4" (i32.const -1)) "disallowing growth")
148148
(assert_return (invoke "load_m4" (i32.const 1)) (i32.const 0))
149149
(assert_return (invoke "store_m4" (i32.const 1) (i32.const 1)))
150150
(assert_return (invoke "load_m4" (i32.const 1)) (i32.const 1))
@@ -178,7 +178,7 @@
178178
(assert_trap (invoke "store_m8" (i32.const -1) (i32.const 0)) "out of bounds memory access")
179179
(assert_return (invoke "grow_m8" (i32.const 1)) (i32.const 1))
180180
(assert_return (invoke "size_m8") (i32.const 2))
181-
(assert_return (invoke "grow_m8" (i32.const -1)) (i32.const -1))
181+
(assert_trap (invoke "grow_m8" (i32.const -1)) "disallowing growth")
182182
(assert_return (invoke "load_m8" (i32.const 1)) (i32.const 0))
183183
(assert_return (invoke "store_m8" (i32.const 1) (i32.const 1)))
184184
(assert_return (invoke "load_m8" (i32.const 1)) (i32.const 1))

0 commit comments

Comments
 (0)