1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
//! Resource-limiter consultation + memory/table growth for `Store<T>`.
//!
//! Split from `mod.rs` to keep it slim. Sync paths (`grow_memory`/`grow_table`,
//! `limiter_allows_*`, the count helpers) error if an async limiter is installed; the
//! `*_async` siblings `.await` it (used by the async driver and `*_async` constructors).
use super::limits::{DEFAULT_MEMORY_CEILING_BYTES, DEFAULT_TABLE_CEILING_ELEMS};
use super::{ResourceLimiter, ResourceLimiterInner, Store, PAGE_SIZE};
use crate::extern_::{Memory, Table};
use crate::value::Ref;
use crate::{Error, Result};
/// Wasm pages → bytes (the limiter works in bytes for memory). Saturating so a hostile `memory64`
/// page count (up to 2^48) can't overflow-panic the limiter check — a saturated `usize::MAX` simply
/// reports an unrepresentable request without truncating it. Backing allocations check it again.
fn bytes(pages: u64) -> usize {
host_count(pages).saturating_mul(PAGE_SIZE)
}
fn host_count(count: u64) -> usize {
usize::try_from(count).unwrap_or(usize::MAX)
}
impl<T: 'static> Store<T> {
/// Runs `f` against an installed *sync* limiter. `Ok(None)` = no limiter; an async
/// limiter on a sync path is an error (use the `*_async` API).
// The `Err` arm only exists under `async`; without it the `Result` looks redundant.
#[allow(clippy::unnecessary_wraps)]
fn with_sync_limiter<R>(
&mut self,
f: impl FnOnce(&mut dyn ResourceLimiter) -> R,
) -> Result<Option<R>> {
match self.limiter.as_mut() {
None => Ok(None),
Some(ResourceLimiterInner::Sync(l)) => Ok(Some(f(l(&mut self.data)))),
#[cfg(feature = "async")]
Some(ResourceLimiterInner::Async(_)) => Err(Error::msg(
"an async resource limiter requires an async context (use the `*_async` API)",
)),
}
}
pub(crate) fn limiter_memories(&mut self) -> Option<usize> {
match self.limiter.as_mut()? {
ResourceLimiterInner::Sync(l) => Some(l(&mut self.data).memories()),
#[cfg(feature = "async")]
ResourceLimiterInner::Async(l) => Some(l(&mut self.data).memories()),
}
}
pub(crate) fn limiter_tables(&mut self) -> Option<usize> {
match self.limiter.as_mut()? {
ResourceLimiterInner::Sync(l) => Some(l(&mut self.data).tables()),
#[cfg(feature = "async")]
ResourceLimiterInner::Async(l) => Some(l(&mut self.data).tables()),
}
}
pub(crate) fn limiter_instances(&mut self) -> Option<usize> {
match self.limiter.as_mut()? {
ResourceLimiterInner::Sync(l) => Some(l(&mut self.data).instances()),
#[cfg(feature = "async")]
ResourceLimiterInner::Async(l) => Some(l(&mut self.data).instances()),
}
}
/// Notifies the limiter of a failed memory grow (`trap_on_grow_failure` → `Err`).
/// `*_grow_failed` is sync on both limiter kinds.
fn memory_grow_failed(&mut self) -> Result<()> {
let err = Error::msg("failed to grow memory");
match self.limiter.as_mut() {
None => Ok(()),
Some(ResourceLimiterInner::Sync(l)) => l(&mut self.data).memory_grow_failed(err),
#[cfg(feature = "async")]
Some(ResourceLimiterInner::Async(l)) => l(&mut self.data).memory_grow_failed(err),
}
}
/// Grows memory `handle` by `delta` pages, consulting the limiter and the
/// declared/architectural maximum. `Ok(Some(old))` grew; `Ok(None)` is a soft
/// failure (return `-1`); `Err` is a trap (`trap_on_grow_failure` / async limiter).
pub(crate) fn grow_memory(&mut self, handle: Memory, delta: u64) -> Result<Option<u64>> {
let (current, max) = {
let e = self.inner.memory(handle);
(e.size_pages(), e.ty.maximum())
};
let desired = current.saturating_add(delta);
let allowed = self
.with_sync_limiter(|l| {
l.memory_growing(bytes(current), bytes(desired), max.map(bytes))
})?
.transpose()?
.unwrap_or(bytes(desired) <= DEFAULT_MEMORY_CEILING_BYTES);
if allowed {
if let Some(old) = self.inner.memory_mut(handle).grow(delta) {
return Ok(Some(old));
}
}
self.memory_grow_failed()?;
Ok(None)
}
/// Grows table `handle` by `delta` elements (filled with `init`); same result
/// convention as [`grow_memory`](Self::grow_memory).
pub(crate) fn grow_table(
&mut self,
handle: Table,
delta: u64,
init: Ref,
) -> Result<Option<u64>> {
let (current, max) = {
let e = self.inner.table(handle);
(e.size() as usize, e.ty.maximum().map(host_count))
};
let desired = current.saturating_add(host_count(delta));
let allowed = self
.with_sync_limiter(|l| l.table_growing(current, desired, max))?
.transpose()?
.unwrap_or(desired <= DEFAULT_TABLE_CEILING_ELEMS);
if allowed {
if let Some(old) = self.inner.table_mut(handle).grow(delta, init) {
return Ok(Some(old));
}
}
let err = Error::msg("failed to grow table");
match self.limiter.as_mut() {
None => {}
Some(ResourceLimiterInner::Sync(l)) => l(&mut self.data).table_grow_failed(err)?,
#[cfg(feature = "async")]
Some(ResourceLimiterInner::Async(l)) => l(&mut self.data).table_grow_failed(err)?,
}
Ok(None)
}
/// Grows the store's GC reservation to `target` bytes, consulting the limiter. The GC heap is
/// accounted like a memory (as in wasmtime) but has **no declared maximum** — that argument is
/// `None`; the limiter is the sole growth bound. A denial traps (guest GC allocation has no
/// soft-fail path). Updates the engine-wide committed-bytes counter that drives the GC-pressure
/// axis. The reservation flow only calls this when `target` exceeds the current reservation.
pub(crate) fn grow_gc_reservation(&mut self, target: usize, bytes_needed: u64) -> Result<()> {
let current = self.inner.gc.reserved();
if target <= current {
return Ok(());
}
// A limiter is the sole bound when installed (the GC heap may grow past the abort cap);
// with no limiter, the abort-safety cap is the bound (the no-limiter finite ceiling).
let allowed = match self.with_sync_limiter(|l| l.memory_growing(current, target, None))? {
Some(decision) => decision?,
None => target <= super::gc::ABORT_SAFETY_CAP,
};
if !allowed {
// Matches wasmtime: a GC allocation the heap can't grow to satisfy is `GcHeapOutOfMemory`,
// not a generic `AllocationTooLarge` trap.
return Err(crate::gc::GcHeapOutOfMemory::new((), bytes_needed).into());
}
let granted = self.inner.gc.grant(target);
self.inner.engine().add_gc_committed(granted);
Ok(())
}
/// Ensures the GC heap can hold a `charge`-byte **host-built** object (`StructRef`/`ArrayRef::new`):
/// collect-then-grow through the limiter, **synchronously**. Unlike the guest path
/// (`Execution::gc_reserve`, which suspends to reach the `T`-generic limiter), host code already
/// holds the `Store<T>`, so it consults the limiter inline. This is a safe point because host
/// code only runs with the execution parked (`invoke_host` / the epoch callback): the guest's
/// live operands are reachable via the parked execution (`exec_roots`) and the call's params
/// via `gc_roots`, so a collection here sees them all. Mirrors `gc_reserve`: growth within
/// the pre-authorized free budget is granted directly (no collection, no limiter); only growth
/// beyond it collects first, then grows through the limiter (a denial traps). Kept in lockstep
/// with `Execution::gc_reserve`.
pub(crate) fn gc_reserve_host(&mut self, charge: usize) -> Result<()> {
if self.inner.gc.fits(charge) {
return Ok(());
}
if !self
.inner
.gc
.is_free_grant(self.inner.gc.desired_reservation(charge))
&& self.inner.gc.is_collecting()
{
// The guest's operands live on the parked execution (this runs inside a host call);
// seed the collection from there so they survive (#27g).
let roots = self.inner.exec_roots()?;
self.inner.gc_collect(&roots)?;
if self.inner.gc.fits(charge) {
return Ok(());
}
}
let target = self.inner.gc.desired_reservation(charge);
if self.inner.gc.is_free_grant(target) {
let granted = self.inner.gc.grant(target);
self.inner.engine().add_gc_committed(granted);
return Ok(());
}
self.grow_gc_reservation(target, charge as u64)
}
/// Host `ExternRef::new`: reserve the entry's footprint through the limiter, then store it
/// (charging the GC budget). Bounded + reclaimable like the GC object heap (#27g).
pub(crate) fn gc_alloc_externref(
&mut self,
value: Box<dyn core::any::Any + Send + Sync>,
) -> Result<u32> {
self.gc_reserve_host(super::entity::extern_charge())?;
self.inner.alloc_externref(value)
}
/// Host `ExnRef::new`: reserve the exception's footprint through the limiter, then store it.
pub(crate) fn gc_alloc_exn(
&mut self,
entity: super::ExnEntity,
) -> Result<crate::value::Rooted<crate::value::ExnRef>> {
self.gc_reserve_host(entity.byte_size())?;
self.inner.alloc_exn(entity)
}
/// Checks the limiter for a brand-new memory of `initial` pages (`Memory::new` or a defined
/// memory at instantiation). With no limiter, the finite default ceiling is the bound.
pub(crate) fn limiter_allows_memory(&mut self, initial: u64, max: Option<u64>) -> Result<bool> {
Ok(self
.with_sync_limiter(|l| l.memory_growing(0, bytes(initial), max.map(bytes)))?
.transpose()?
.unwrap_or(bytes(initial) <= DEFAULT_MEMORY_CEILING_BYTES))
}
/// Checks the limiter for a brand-new table of `initial` elements (`Table::new` or a defined
/// table at instantiation). With no limiter, the finite default ceiling is the bound.
pub(crate) fn limiter_allows_table(&mut self, initial: u64, max: Option<u64>) -> Result<bool> {
Ok(self
.with_sync_limiter(|l| l.table_growing(0, host_count(initial), max.map(host_count)))?
.transpose()?
.unwrap_or(host_count(initial) <= DEFAULT_TABLE_CEILING_ELEMS))
}
}
#[cfg(feature = "async")]
impl<T: 'static> Store<T> {
/// Consults the limiter for a memory grow `current → desired` bytes, awaiting an
/// async limiter. No limiter ⇒ allowed.
async fn memory_growing_async(
&mut self,
current: usize,
desired: usize,
max: Option<usize>,
) -> Result<bool> {
match &mut self.limiter {
None => Ok(desired <= DEFAULT_MEMORY_CEILING_BYTES),
Some(ResourceLimiterInner::Sync(l)) => {
l(&mut self.data).memory_growing(current, desired, max)
}
Some(ResourceLimiterInner::Async(l)) => {
l(&mut self.data)
.memory_growing(current, desired, max)
.await
}
}
}
/// Async sibling of [`grow_memory`](Self::grow_memory): awaits the limiter.
pub(crate) async fn grow_memory_async(
&mut self,
handle: Memory,
delta: u64,
) -> Result<Option<u64>> {
let (current, max) = {
let e = self.inner.memory(handle);
(e.size_pages(), e.ty.maximum())
};
let desired = current.saturating_add(delta);
let allowed = self
.memory_growing_async(bytes(current), bytes(desired), max.map(bytes))
.await?;
if allowed {
if let Some(old) = self.inner.memory_mut(handle).grow(delta) {
return Ok(Some(old));
}
}
self.memory_grow_failed()?;
Ok(None)
}
/// Async sibling of [`limiter_allows_memory`](Self::limiter_allows_memory) (`Memory::new_async`).
pub(crate) async fn limiter_allows_memory_async(
&mut self,
initial: u64,
max: Option<u64>,
) -> Result<bool> {
self.memory_growing_async(0, bytes(initial), max.map(bytes))
.await
}
/// Consults the limiter for a table grow `current → desired` elements, awaiting an
/// async limiter. No limiter ⇒ allowed.
async fn table_growing_async(
&mut self,
current: usize,
desired: usize,
max: Option<usize>,
) -> Result<bool> {
match &mut self.limiter {
None => Ok(desired <= DEFAULT_TABLE_CEILING_ELEMS),
Some(ResourceLimiterInner::Sync(l)) => {
l(&mut self.data).table_growing(current, desired, max)
}
Some(ResourceLimiterInner::Async(l)) => {
l(&mut self.data).table_growing(current, desired, max).await
}
}
}
/// Async sibling of [`limiter_allows_table`](Self::limiter_allows_table) (`Table::new_async`).
pub(crate) async fn limiter_allows_table_async(
&mut self,
initial: u64,
max: Option<u64>,
) -> Result<bool> {
self.table_growing_async(0, host_count(initial), max.map(host_count))
.await
}
/// Async sibling of [`grow_table`](Self::grow_table): awaits the limiter.
pub(crate) async fn grow_table_async(
&mut self,
handle: Table,
delta: u64,
init: Ref,
) -> Result<Option<u64>> {
let (current, max) = {
let e = self.inner.table(handle);
(e.size() as usize, e.ty.maximum().map(host_count))
};
let desired = current.saturating_add(host_count(delta));
let allowed = self.table_growing_async(current, desired, max).await?;
if allowed {
if let Some(old) = self.inner.table_mut(handle).grow(delta, init) {
return Ok(Some(old));
}
}
let err = Error::msg("failed to grow table");
match self.limiter.as_mut() {
None => {}
Some(ResourceLimiterInner::Sync(l)) => l(&mut self.data).table_grow_failed(err)?,
Some(ResourceLimiterInner::Async(l)) => l(&mut self.data).table_grow_failed(err)?,
}
Ok(None)
}
/// Async sibling of [`gc_reserve_host`](Self::gc_reserve_host): awaits an async limiter when
/// host code allocates GC objects from an async context.
pub(crate) async fn gc_reserve_host_async(&mut self, charge: usize) -> Result<()> {
if self.inner.gc.fits(charge) {
return Ok(());
}
if !self
.inner
.gc
.is_free_grant(self.inner.gc.desired_reservation(charge))
&& self.inner.gc.is_collecting()
{
let roots = self.inner.exec_roots()?;
self.inner.gc_collect(&roots)?;
if self.inner.gc.fits(charge) {
return Ok(());
}
}
let target = self.inner.gc.desired_reservation(charge);
if self.inner.gc.is_free_grant(target) {
let granted = self.inner.gc.grant(target);
self.inner.engine().add_gc_committed(granted);
return Ok(());
}
let current = self.inner.gc.reserved();
let allowed = match &mut self.limiter {
None => target <= super::gc::ABORT_SAFETY_CAP,
Some(ResourceLimiterInner::Sync(l)) => {
l(&mut self.data).memory_growing(current, target, None)?
}
Some(ResourceLimiterInner::Async(l)) => {
l(&mut self.data)
.memory_growing(current, target, None)
.await?
}
};
if !allowed {
return Err(crate::gc::GcHeapOutOfMemory::new((), charge as u64).into());
}
let granted = self.inner.gc.grant(target);
self.inner.engine().add_gc_committed(granted);
Ok(())
}
}