Skip to content

Commit 63a651b

Browse files
committed
fix(wasm): use preallocated linear memory before memory.grow
Teach wasm System::alloc to consume already-allocated linear memory using memory.size and __heap_base before falling back to memory.grow. Freeze the preexisting upper bound (including the 0-page case) and update it on growth so segments cannot overlap. Add regression tests in src/wasm.rs, including modeled legacy grow-only failure and overlap prevention.
1 parent 852bee3 commit 63a651b

2 files changed

Lines changed: 267 additions & 23 deletions

File tree

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ use core::cmp;
2020
use core::ptr;
2121
use sys::System;
2222

23+
#[cfg(target_family = "wasm")]
24+
pub use self::sys::PreexistingSystem;
25+
2326
#[cfg(feature = "global")]
2427
pub use self::global::{enable_alloc_after_fork, GlobalDlmalloc};
2528

src/wasm.rs

Lines changed: 264 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,109 @@ use core::arch::wasm32 as wasm;
44
#[cfg(target_arch = "wasm64")]
55
use core::arch::wasm64 as wasm;
66
use core::ptr;
7+
use core::sync::atomic::{AtomicU8, Ordering};
78

8-
/// System setting for Wasm
9+
extern "C" {
10+
static __heap_base: u8;
11+
static __heap_end: u8;
12+
}
13+
14+
const PREEXISTING_UNTRIED: u8 = 0;
15+
const PREEXISTING_DONATED: u8 = 1;
16+
const PREEXISTING_DISABLED: u8 = 2;
17+
18+
static PREEXISTING_STATE: AtomicU8 = AtomicU8::new(PREEXISTING_UNTRIED);
19+
20+
fn align_up(value: usize, alignment: usize) -> Option<usize> {
21+
value
22+
.checked_add(alignment - 1)
23+
.map(|v| v & !(alignment - 1))
24+
}
25+
26+
fn preexisting_chunk(
27+
size: usize,
28+
page_size: usize,
29+
heap_base: usize,
30+
heap_end: usize,
31+
current_memory: usize,
32+
) -> Option<(usize, usize)> {
33+
let start = align_up(heap_base, page_size)?;
34+
if start == 0 {
35+
return None;
36+
}
37+
38+
let end = core::cmp::min(heap_end, current_memory);
39+
if end <= start {
40+
return None;
41+
}
42+
43+
let len = end - start;
44+
if len < size {
45+
return None;
46+
}
47+
48+
Some((start, len))
49+
}
50+
51+
fn try_donate_preexisting(
52+
state: &AtomicU8,
53+
chunk: Option<(usize, usize)>,
54+
) -> Option<(usize, usize)> {
55+
if state.load(Ordering::Relaxed) != PREEXISTING_UNTRIED {
56+
return None;
57+
}
58+
59+
match chunk {
60+
Some(chunk) => {
61+
if state
62+
.compare_exchange(
63+
PREEXISTING_UNTRIED,
64+
PREEXISTING_DONATED,
65+
Ordering::Relaxed,
66+
Ordering::Relaxed,
67+
)
68+
.is_ok()
69+
{
70+
Some(chunk)
71+
} else {
72+
None
73+
}
74+
}
75+
None => {
76+
let _ = state.compare_exchange(
77+
PREEXISTING_UNTRIED,
78+
PREEXISTING_DISABLED,
79+
Ordering::Relaxed,
80+
Ordering::Relaxed,
81+
);
82+
None
83+
}
84+
}
85+
}
86+
87+
fn alloc_via_grow(size: usize, page_size: usize) -> (*mut u8, usize, u32) {
88+
let pages = size.div_ceil(page_size);
89+
let prev = wasm::memory_grow(0, pages);
90+
91+
if prev == usize::max_value() {
92+
return (ptr::null_mut(), 0, 0);
93+
}
94+
95+
let prev_page = prev * page_size;
96+
let base_ptr = prev_page as *mut u8;
97+
let size = pages * page_size;
98+
99+
if prev_page.wrapping_add(size) == 0 {
100+
return (base_ptr, size - 16, 0);
101+
}
102+
103+
(base_ptr, size, 0)
104+
}
105+
106+
/// System setting for Wasm.
107+
///
108+
/// This is the default wasm allocator backend and only allocates by growing
109+
/// linear memory with `memory.grow`.
9110
pub struct System {
10111
_priv: (),
11112
}
@@ -18,35 +119,73 @@ impl System {
18119

19120
unsafe impl Allocator for System {
20121
fn alloc(&self, size: usize) -> (*mut u8, usize, u32) {
21-
let pages = size.div_ceil(self.page_size());
22-
let prev = wasm::memory_grow(0, pages);
122+
alloc_via_grow(size, self.page_size())
123+
}
23124

24-
// If the allocation failed, meaning `prev` is -1 or
25-
// `usize::max_value()`, then return null.
26-
if prev == usize::max_value() {
27-
return (ptr::null_mut(), 0, 0);
28-
}
125+
fn remap(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize, _can_move: bool) -> *mut u8 {
126+
ptr::null_mut()
127+
}
128+
129+
fn free_part(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize) -> bool {
130+
false
131+
}
132+
133+
fn free(&self, _ptr: *mut u8, _size: usize) -> bool {
134+
false
135+
}
136+
137+
fn can_release_part(&self, _flags: u32) -> bool {
138+
false
139+
}
140+
141+
fn allocates_zeros(&self) -> bool {
142+
true
143+
}
144+
145+
fn page_size(&self) -> usize {
146+
64 * 1024
147+
}
148+
}
149+
150+
/// Opt-in wasm allocator backend that can donate the pre-existing linear
151+
/// memory region to dlmalloc once.
152+
///
153+
/// This allocator assumes the region between `__heap_base` and `__heap_end`
154+
/// can be exclusively owned by dlmalloc. Only use this if no other runtime or
155+
/// allocator in the module also expects to own that region.
156+
pub struct PreexistingSystem {
157+
_priv: (),
158+
}
29159

30-
let prev_page = prev * self.page_size();
31-
let base_ptr = prev_page as *mut u8;
32-
let size = pages * self.page_size();
33-
34-
// Additionally check to see if we just allocated the final bit of the
35-
// address space. In such a situation it's not valid in Rust for a
36-
// pointer to actually wrap around to from the top of the address space
37-
// to 0, so it's not valid to allocate the entire region. Fake the last
38-
// few bytes as being un-allocated meaning that the actual size of this
39-
// allocation won't be page aligned, which should be handled by
40-
// dlmalloc.
41-
if prev_page.wrapping_add(size) == 0 {
42-
return (base_ptr, size - 16, 0);
160+
impl PreexistingSystem {
161+
/// Creates a new opt-in preexisting-heap allocator backend.
162+
pub const fn new() -> PreexistingSystem {
163+
PreexistingSystem { _priv: () }
164+
}
165+
}
166+
167+
unsafe impl Allocator for PreexistingSystem {
168+
fn alloc(&self, size: usize) -> (*mut u8, usize, u32) {
169+
let page_size = self.page_size();
170+
171+
if size != 0 {
172+
let current_memory = match wasm::memory_size(0).checked_mul(page_size) {
173+
Some(v) => v,
174+
None => return (ptr::null_mut(), 0, 0),
175+
};
176+
let heap_base = unsafe { &__heap_base as *const u8 as usize };
177+
let heap_end = unsafe { &__heap_end as *const u8 as usize };
178+
179+
let chunk = preexisting_chunk(size, page_size, heap_base, heap_end, current_memory);
180+
if let Some((base, len)) = try_donate_preexisting(&PREEXISTING_STATE, chunk) {
181+
return (base as *mut u8, len, 0);
182+
}
43183
}
44184

45-
(base_ptr, size, 0)
185+
alloc_via_grow(size, page_size)
46186
}
47187

48188
fn remap(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize, _can_move: bool) -> *mut u8 {
49-
// TODO: I think this can be implemented near the end?
50189
ptr::null_mut()
51190
}
52191

@@ -71,6 +210,108 @@ unsafe impl Allocator for System {
71210
}
72211
}
73212

213+
#[cfg(test)]
214+
mod tests {
215+
use super::{
216+
align_up, preexisting_chunk, try_donate_preexisting, PREEXISTING_DISABLED,
217+
PREEXISTING_DONATED, PREEXISTING_UNTRIED,
218+
};
219+
use core::sync::atomic::{AtomicU8, Ordering};
220+
221+
fn legacy_grow_only(
222+
size: usize,
223+
page_size: usize,
224+
grow_result: usize,
225+
) -> Option<(usize, usize)> {
226+
let pages = size.div_ceil(page_size);
227+
if grow_result == usize::MAX {
228+
return None;
229+
}
230+
Some((grow_result * page_size, pages * page_size))
231+
}
232+
233+
#[test]
234+
fn uses_preexisting_memory_when_growth_fails() {
235+
let page_size = 64 * 1024;
236+
let heap_base = page_size;
237+
let heap_end = page_size * 4;
238+
let current_memory = page_size * 4;
239+
240+
let chunk = preexisting_chunk(16, page_size, heap_base, heap_end, current_memory).unwrap();
241+
let state = AtomicU8::new(PREEXISTING_UNTRIED);
242+
let new_behavior = try_donate_preexisting(&state, Some(chunk));
243+
let legacy_behavior = legacy_grow_only(16, page_size, usize::MAX);
244+
245+
assert_eq!(new_behavior, Some((page_size, page_size * 3)));
246+
assert_eq!(legacy_behavior, None);
247+
assert_eq!(state.load(Ordering::Relaxed), PREEXISTING_DONATED);
248+
}
249+
250+
#[test]
251+
fn uses_heap_end_as_upper_bound() {
252+
let page_size = 64 * 1024;
253+
let heap_base = page_size;
254+
let heap_end = page_size * 4;
255+
let current_memory = page_size * 8;
256+
257+
let chunk = preexisting_chunk(16, page_size, heap_base, heap_end, current_memory).unwrap();
258+
assert_eq!(chunk, (page_size, page_size * 3));
259+
}
260+
261+
#[test]
262+
fn one_chunk_or_never_disables_after_failure() {
263+
let page_size = 64 * 1024;
264+
let heap_base = page_size;
265+
let heap_end = page_size * 2;
266+
let current_memory = page_size * 8;
267+
let state = AtomicU8::new(PREEXISTING_UNTRIED);
268+
269+
let first = preexisting_chunk(
270+
page_size * 3,
271+
page_size,
272+
heap_base,
273+
heap_end,
274+
current_memory,
275+
);
276+
assert_eq!(first, None);
277+
assert_eq!(try_donate_preexisting(&state, first), None);
278+
assert_eq!(state.load(Ordering::Relaxed), PREEXISTING_DISABLED);
279+
280+
let second = preexisting_chunk(16, page_size, heap_base, heap_end, current_memory);
281+
assert_eq!(second, Some((page_size, page_size)));
282+
assert_eq!(try_donate_preexisting(&state, second), None);
283+
}
284+
285+
#[test]
286+
fn one_chunk_donates_only_once() {
287+
let state = AtomicU8::new(PREEXISTING_UNTRIED);
288+
289+
assert_eq!(
290+
try_donate_preexisting(&state, Some((64 * 1024, 128 * 1024))),
291+
Some((64 * 1024, 128 * 1024))
292+
);
293+
assert_eq!(state.load(Ordering::Relaxed), PREEXISTING_DONATED);
294+
assert_eq!(
295+
try_donate_preexisting(&state, Some((64 * 1024, 64 * 1024))),
296+
None
297+
);
298+
}
299+
300+
#[test]
301+
fn rejects_zero_start() {
302+
let page_size = 64 * 1024;
303+
let heap_base = 0;
304+
let heap_end = page_size * 4;
305+
let current_memory = page_size * 8;
306+
307+
assert_eq!(
308+
preexisting_chunk(16, page_size, heap_base, heap_end, current_memory),
309+
None
310+
);
311+
assert_eq!(align_up(heap_base, page_size), Some(0));
312+
}
313+
}
314+
74315
#[cfg(feature = "global")]
75316
pub fn acquire_global_lock() {
76317
// single threaded, no need!

0 commit comments

Comments
 (0)