@wasm-oj/server 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,18 +7,13 @@ use std::borrow::Cow;
7
7
  use std::collections::BTreeMap;
8
8
  use wasm_encoder::reencode::{Error as ReencodeError, Reencode};
9
9
  use wasm_encoder::{Encode, Section};
10
- #[cfg(not(target_arch = "wasm32"))]
11
- use wasmer::Global;
12
10
  #[cfg(target_arch = "wasm32")]
13
11
  use wasmer::js::AsJs;
14
- use wasmer::{AsStoreMut, Instance};
12
+ use wasmer::{AsStoreMut, Function, Global, Imports, Instance};
15
13
 
16
14
  pub const METER_MODEL: &str = "weighted";
17
15
  const METERING_MODULE: &str = "wasm_oj_metering";
18
16
  const GAS_COUNTER_NAME: &str = "gas_counter";
19
- pub(crate) const CONTESTANT_METERING_MODULE: &str = "wasm_oj_contestant_metering";
20
- pub(crate) const INTERACTOR_METERING_MODULE: &str = "wasm_oj_interactor_metering";
21
- pub(crate) const HOST_GAS_FUNCTION: &str = "charge";
22
17
 
23
18
  #[derive(Debug)]
24
19
  pub struct InstrumentedModule {
@@ -53,10 +48,7 @@ pub enum CostPoints {
53
48
 
54
49
  #[derive(Clone, Debug)]
55
50
  pub struct MeterState {
56
- #[cfg(not(target_arch = "wasm32"))]
57
51
  gas_counter: Global,
58
- #[cfg(target_arch = "wasm32")]
59
- gas_counter: WebAssembly::Global,
60
52
  }
61
53
 
62
54
  pub fn instrument_wasm(wasm: &[u8], budget: u64) -> Result<InstrumentedModule, String> {
@@ -85,32 +77,6 @@ pub fn instrument_wasm(wasm: &[u8], budget: u64) -> Result<InstrumentedModule, S
85
77
  })
86
78
  }
87
79
 
88
- pub(crate) fn instrument_wasm_with_host_meter(
89
- wasm: &[u8],
90
- metering_module: &'static str,
91
- ) -> Result<InstrumentedModule, String> {
92
- let runtime_sections = runtime_custom_sections(wasm)?;
93
- let executable = canonicalize_custom_sections(wasm)?;
94
- let operations = inspect_weighted_opcodes(&executable)?;
95
- let mut module = ModuleInfo::new(&executable)
96
- .map_err(|error| format!("failed to parse module for weighted metering: {error}"))?;
97
- let backend = gas_metering::host_function::Injector::new(metering_module, HOST_GAS_FUNCTION);
98
- let mut metered = gas_metering::inject(&mut module, backend, &WeightedRules)
99
- .map_err(|error| format!("failed to inject weighted host metering: {error}"))?;
100
- for (name, data) in runtime_sections {
101
- let section = wasm_encoder::CustomSection {
102
- name: Cow::Owned(name),
103
- data: Cow::Owned(data),
104
- };
105
- metered.push(section.id());
106
- section.encode(&mut metered);
107
- }
108
- Ok(InstrumentedModule {
109
- wasm: metered,
110
- operations,
111
- })
112
- }
113
-
114
80
  #[derive(Debug)]
115
81
  struct MeterInitializationError(String);
116
82
 
@@ -122,30 +88,424 @@ impl std::fmt::Display for MeterInitializationError {
122
88
 
123
89
  impl std::error::Error for MeterInitializationError {}
124
90
 
91
+ /// Units a program may spend between safepoints.
92
+ pub const SAFEPOINT_INTERVAL: i64 = 1 << 20;
93
+ const SAFEPOINT_NAME: &str = "safepoint";
94
+
95
+ /// Final instrumentation pass: sets the meter's budget, adds the uncharged safepoint, and gives
96
+ /// every function that has a loop but no parameters or locals one unused `i32` local.
97
+ ///
98
+ /// JavaScriptCore (Safari 26) never enters optimized code inside a loop of a function without
99
+ /// parameters or locals: its baseline tier asks to tier up on almost every iteration and keeps
100
+ /// running the slow path, so a metered empty loop runs about 20 times slower than in Chromium and
101
+ /// hits the wall deadline before its instruction budget. The local changes neither behaviour nor
102
+ /// cost.
103
+ ///
104
+ /// JavaScriptCore also never stops a terminated Worker while it runs Wasm, only when it next
105
+ /// reaches a JavaScript checkpoint such as `Atomics.wait`. So the charge that opens every function
106
+ /// and loop body is inlined, and once the counter drops below a threshold it reaches a cold
107
+ /// function that traps on exhaustion, or calls the imported `wasm_oj_metering.safepoint` (which
108
+ /// the browser host turns into such a checkpoint) and lowers the threshold by
109
+ /// `SAFEPOINT_INTERVAL`. A call inside a hot loop makes the whole loop slower in JavaScriptCore and
110
+ /// V8 even when it never runs, so a loop body leaves the loop to reach it: each loop becomes
111
+ /// `block (loop (block (loop ...) br 2) safepoint, refund, br 0)`, and every branch out of it
112
+ /// skips the three new labels. Functions that use other kinds of labels keep the call inside the
113
+ /// loop. Every other block keeps radix's gas function. Exhaustion is exactly where it was, and
114
+ /// none of these instructions is charged.
125
115
  struct MeterInitializer {
126
116
  budget: i64,
117
+ parameterized_types: Vec<bool>,
118
+ function_types: Vec<u32>,
119
+ defined_functions: usize,
120
+ original_types: u32,
121
+ imported_functions: u32,
122
+ imported_globals: u32,
123
+ defined_globals: u32,
124
+ import_section_written: bool,
125
+ }
126
+
127
+ impl MeterInitializer {
128
+ fn new(budget: i64) -> Self {
129
+ Self {
130
+ budget,
131
+ parameterized_types: Vec::new(),
132
+ function_types: Vec::new(),
133
+ defined_functions: 0,
134
+ original_types: 0,
135
+ imported_functions: 0,
136
+ imported_globals: 0,
137
+ defined_globals: 0,
138
+ import_section_written: false,
139
+ }
140
+ }
141
+
142
+ fn safepoint_type(&self) -> u32 {
143
+ self.original_types
144
+ }
145
+
146
+ fn safepoint_function(&self) -> u32 {
147
+ self.imported_functions
148
+ }
149
+
150
+ fn gas_function(&self) -> u32 {
151
+ self.imported_functions + self.function_types.len() as u32
152
+ }
153
+
154
+ fn slow_function(&self) -> u32 {
155
+ self.gas_function() + 1
156
+ }
157
+
158
+ fn gas_global(&self) -> u32 {
159
+ self.imported_globals + self.defined_globals - 1
160
+ }
161
+
162
+ fn threshold_global(&self) -> u32 {
163
+ self.imported_globals + self.defined_globals
164
+ }
165
+
166
+ fn safepoint_import(&self, imports: &mut wasm_encoder::ImportSection) {
167
+ imports.import(
168
+ METERING_MODULE,
169
+ SAFEPOINT_NAME,
170
+ wasm_encoder::EntityType::Function(self.safepoint_type()),
171
+ );
172
+ }
173
+
174
+ fn slow_body(&self) -> wasm_encoder::Function {
175
+ use wasm_encoder::Instruction::*;
176
+ let gas = self.gas_global();
177
+ let mut function = wasm_encoder::Function::new([(1, wasm_encoder::ValType::I64)]);
178
+ for instruction in [
179
+ GlobalGet(gas),
180
+ I64Const(0),
181
+ I64LtS,
182
+ If(wasm_encoder::BlockType::Empty),
183
+ I64Const(-1),
184
+ GlobalSet(gas),
185
+ Unreachable,
186
+ End,
187
+ GlobalGet(gas),
188
+ I64Const(SAFEPOINT_INTERVAL),
189
+ I64Sub,
190
+ LocalTee(0),
191
+ I64Const(0),
192
+ LocalGet(0),
193
+ I64Const(0),
194
+ I64GtS,
195
+ Select,
196
+ GlobalSet(self.threshold_global()),
197
+ Call(self.safepoint_function()),
198
+ End,
199
+ ] {
200
+ function.instruction(&instruction);
201
+ }
202
+ function
203
+ }
204
+
205
+ /// Re-encodes a body, replacing the gas call that opens the function and each loop body with
206
+ /// an inline charge and safepoint check.
207
+ fn encode_body(
208
+ &mut self,
209
+ body: &wasmparser::FunctionBody<'_>,
210
+ extra_local: bool,
211
+ ) -> Result<wasm_encoder::Function, ReencodeError<MeterInitializationError>> {
212
+ use wasm_encoder::Instruction::*;
213
+ use wasmparser::Operator as Op;
214
+ let mut function = self.new_function_with_parsed_locals(body)?;
215
+ if extra_local {
216
+ function = wasm_encoder::Function::new([(1, wasm_encoder::ValType::I32)]);
217
+ }
218
+ let original_gas = self.imported_functions + self.function_types.len() as u32 - 1;
219
+ let gas = self.gas_global();
220
+ let rotate = !has_other_labels(body)?;
221
+ let mut frames: Vec<Option<i64>> = Vec::new();
222
+ let mut head = Some(false);
223
+ let mut pending = None;
224
+ let adjust = |frames: &[Option<i64>], depth: u32| {
225
+ let crossed = frames[frames.len() - depth as usize..]
226
+ .iter()
227
+ .filter(|frame| frame.is_some())
228
+ .count();
229
+ depth + 3 * crossed as u32
230
+ };
231
+ let mut operators = body.get_operators_reader()?;
232
+ while !operators.eof() {
233
+ let operator = operators.read()?;
234
+ if let Some((cost, wrapped)) = pending.take() {
235
+ if matches!(operator, Op::Call { function_index } if function_index == original_gas)
236
+ {
237
+ for instruction in [
238
+ GlobalGet(gas),
239
+ I64Const(cost),
240
+ I64Sub,
241
+ GlobalSet(gas),
242
+ GlobalGet(gas),
243
+ GlobalGet(self.threshold_global()),
244
+ I64LtS,
245
+ ] {
246
+ function.instruction(&instruction);
247
+ }
248
+ if wrapped {
249
+ function.instruction(&BrIf(1));
250
+ if let Some(frame) = frames.last_mut() {
251
+ *frame = Some(cost);
252
+ }
253
+ } else {
254
+ function.instruction(&If(wasm_encoder::BlockType::Empty));
255
+ function.instruction(&Call(self.slow_function()));
256
+ function.instruction(&End);
257
+ }
258
+ continue;
259
+ }
260
+ function.instruction(&I64Const(cost));
261
+ }
262
+ let at_head = head.take();
263
+ match operator {
264
+ Op::I64Const { value } if at_head.is_some() => {
265
+ pending = Some((value, at_head == Some(true)));
266
+ continue;
267
+ }
268
+ Op::Loop { blockty } => {
269
+ let parameterized = match blockty {
270
+ wasmparser::BlockType::FuncType(ty) => self
271
+ .parameterized_types
272
+ .get(ty as usize)
273
+ .copied()
274
+ .unwrap_or(true),
275
+ _ => false,
276
+ };
277
+ let wrapped = rotate && !parameterized;
278
+ let blockty = self.block_type(blockty)?;
279
+ if wrapped {
280
+ function.instruction(&Block(blockty));
281
+ function.instruction(&Loop(wasm_encoder::BlockType::Empty));
282
+ function.instruction(&Block(wasm_encoder::BlockType::Empty));
283
+ frames.push(Some(0));
284
+ } else {
285
+ frames.push(None);
286
+ }
287
+ function.instruction(&Loop(blockty));
288
+ head = Some(wrapped);
289
+ continue;
290
+ }
291
+ Op::Block { .. } | Op::If { .. } | Op::Try { .. } | Op::TryTable { .. } => {
292
+ frames.push(None)
293
+ }
294
+ Op::End => {
295
+ if let Some(Some(cost)) = frames.pop() {
296
+ for instruction in [
297
+ End,
298
+ Br(2),
299
+ End,
300
+ Call(self.slow_function()),
301
+ GlobalGet(gas),
302
+ I64Const(cost),
303
+ I64Add,
304
+ GlobalSet(gas),
305
+ Br(0),
306
+ End,
307
+ Unreachable,
308
+ End,
309
+ ] {
310
+ function.instruction(&instruction);
311
+ }
312
+ continue;
313
+ }
314
+ }
315
+ Op::Br { relative_depth } if rotate => {
316
+ function.instruction(&Br(adjust(&frames, relative_depth)));
317
+ continue;
318
+ }
319
+ Op::BrIf { relative_depth } if rotate => {
320
+ function.instruction(&BrIf(adjust(&frames, relative_depth)));
321
+ continue;
322
+ }
323
+ Op::BrTable { targets } if rotate => {
324
+ let mut depths = Vec::new();
325
+ for target in targets.targets() {
326
+ depths.push(adjust(&frames, target?));
327
+ }
328
+ function
329
+ .instruction(&BrTable(depths.into(), adjust(&frames, targets.default())));
330
+ continue;
331
+ }
332
+ _ => {}
333
+ }
334
+ function.instruction(&self.instruction(operator)?);
335
+ }
336
+ Ok(function)
337
+ }
338
+ }
339
+
340
+ /// Whether a body uses labels other than through `br`, `br_if` and `br_table`.
341
+ fn has_other_labels(
342
+ body: &wasmparser::FunctionBody<'_>,
343
+ ) -> Result<bool, wasmparser::BinaryReaderError> {
344
+ use wasmparser::Operator as Op;
345
+ let mut operators = body.get_operators_reader()?;
346
+ while !operators.eof() {
347
+ if matches!(
348
+ operators.read()?,
349
+ Op::Try { .. }
350
+ | Op::TryTable { .. }
351
+ | Op::Delegate { .. }
352
+ | Op::Rethrow { .. }
353
+ | Op::BrOnNull { .. }
354
+ | Op::BrOnNonNull { .. }
355
+ | Op::BrOnCast { .. }
356
+ | Op::BrOnCastFail { .. }
357
+ | Op::BrOnCastDescEq { .. }
358
+ | Op::BrOnCastDescEqFail { .. }
359
+ ) {
360
+ return Ok(true);
361
+ }
362
+ }
363
+ Ok(false)
364
+ }
365
+
366
+ fn meter_error(message: &str) -> ReencodeError<MeterInitializationError> {
367
+ ReencodeError::UserError(MeterInitializationError(message.to_string()))
127
368
  }
128
369
 
129
370
  impl Reencode for MeterInitializer {
130
371
  type Error = MeterInitializationError;
131
372
 
373
+ fn function_index(&mut self, func: u32) -> Result<u32, ReencodeError<Self::Error>> {
374
+ Ok(if func >= self.imported_functions {
375
+ func + 1
376
+ } else {
377
+ func
378
+ })
379
+ }
380
+
381
+ fn intersperse_section_hook(
382
+ &mut self,
383
+ module: &mut wasm_encoder::Module,
384
+ _after: Option<wasm_encoder::SectionId>,
385
+ before: Option<wasm_encoder::SectionId>,
386
+ ) -> Result<(), ReencodeError<Self::Error>> {
387
+ use wasm_encoder::SectionId;
388
+ if self.import_section_written
389
+ || matches!(
390
+ before,
391
+ Some(SectionId::Custom | SectionId::Type | SectionId::Import)
392
+ )
393
+ {
394
+ return Ok(());
395
+ }
396
+ let mut imports = wasm_encoder::ImportSection::new();
397
+ self.safepoint_import(&mut imports);
398
+ module.section(&imports);
399
+ self.import_section_written = true;
400
+ Ok(())
401
+ }
402
+
403
+ fn parse_type_section(
404
+ &mut self,
405
+ types: &mut wasm_encoder::TypeSection,
406
+ section: wasmparser::TypeSectionReader<'_>,
407
+ ) -> Result<(), ReencodeError<Self::Error>> {
408
+ for group in section.clone() {
409
+ for ty in group?.types() {
410
+ self.parameterized_types
411
+ .push(match &ty.composite_type.inner {
412
+ wasmparser::CompositeInnerType::Func(function) => {
413
+ !function.params().is_empty()
414
+ }
415
+ _ => true,
416
+ });
417
+ }
418
+ }
419
+ self.original_types = self.parameterized_types.len() as u32;
420
+ wasm_encoder::reencode::utils::parse_type_section(self, types, section)?;
421
+ types.ty().function([], []);
422
+ Ok(())
423
+ }
424
+
425
+ fn parse_import_section(
426
+ &mut self,
427
+ imports: &mut wasm_encoder::ImportSection,
428
+ section: wasmparser::ImportSectionReader<'_>,
429
+ ) -> Result<(), ReencodeError<Self::Error>> {
430
+ for import in section.clone().into_imports() {
431
+ match import?.ty {
432
+ wasmparser::TypeRef::Func(_) => self.imported_functions += 1,
433
+ wasmparser::TypeRef::Global(_) => self.imported_globals += 1,
434
+ _ => {}
435
+ }
436
+ }
437
+ wasm_encoder::reencode::utils::parse_import_section(self, imports, section)?;
438
+ self.safepoint_import(imports);
439
+ self.import_section_written = true;
440
+ Ok(())
441
+ }
442
+
443
+ fn parse_function_section(
444
+ &mut self,
445
+ functions: &mut wasm_encoder::FunctionSection,
446
+ section: wasmparser::FunctionSectionReader<'_>,
447
+ ) -> Result<(), ReencodeError<Self::Error>> {
448
+ for function in section.clone() {
449
+ self.function_types.push(function?);
450
+ }
451
+ if self.function_types.is_empty() {
452
+ return Err(meter_error("instrumented module has no gas function"));
453
+ }
454
+ wasm_encoder::reencode::utils::parse_function_section(self, functions, section)?;
455
+ functions.function(self.safepoint_type());
456
+ Ok(())
457
+ }
458
+
459
+ fn parse_code_section(
460
+ &mut self,
461
+ code: &mut wasm_encoder::CodeSection,
462
+ section: wasmparser::CodeSectionReader<'_>,
463
+ ) -> Result<(), ReencodeError<Self::Error>> {
464
+ wasm_encoder::reencode::utils::parse_code_section(self, code, section)?;
465
+ code.function(&self.slow_body());
466
+ Ok(())
467
+ }
468
+
469
+ fn parse_function_body(
470
+ &mut self,
471
+ code: &mut wasm_encoder::CodeSection,
472
+ body: wasmparser::FunctionBody<'_>,
473
+ ) -> Result<(), ReencodeError<Self::Error>> {
474
+ let ordinal = self.defined_functions;
475
+ self.defined_functions += 1;
476
+ if ordinal + 1 == self.function_types.len() {
477
+ return wasm_encoder::reencode::utils::parse_function_body(self, code, body);
478
+ }
479
+ let parameterized = self
480
+ .function_types
481
+ .get(ordinal)
482
+ .and_then(|ty| self.parameterized_types.get(*ty as usize))
483
+ .copied()
484
+ .unwrap_or(true);
485
+ let loops = has_loop(&body)?;
486
+ let extra_local = !parameterized && body.get_locals_reader()?.get_count() == 0 && loops;
487
+ let function = self.encode_body(&body, extra_local)?;
488
+ code.function(&function);
489
+ Ok(())
490
+ }
491
+
132
492
  fn parse_global_section(
133
493
  &mut self,
134
494
  globals: &mut wasm_encoder::GlobalSection,
135
495
  section: wasmparser::GlobalSectionReader<'_>,
136
496
  ) -> Result<(), ReencodeError<Self::Error>> {
137
- let meter_ordinal = section.count().checked_sub(1).ok_or_else(|| {
138
- ReencodeError::UserError(MeterInitializationError(
139
- "instrumented module has no meter global".to_string(),
140
- ))
141
- })?;
497
+ self.defined_globals = section.count();
498
+ let meter_ordinal = section
499
+ .count()
500
+ .checked_sub(1)
501
+ .ok_or_else(|| meter_error("instrumented module has no meter global"))?;
142
502
  for (ordinal, global) in section.into_iter().enumerate() {
143
503
  let global = global?;
144
504
  if u32::try_from(ordinal).ok() == Some(meter_ordinal) {
145
505
  if global.ty.content_type != wasmparser::ValType::I64 || !global.ty.mutable {
146
- return Err(ReencodeError::UserError(MeterInitializationError(
147
- "instrumented meter global has an unexpected type".to_string(),
148
- )));
506
+ return Err(meter_error(
507
+ "instrumented meter global has an unexpected type",
508
+ ));
149
509
  }
150
510
  globals.global(
151
511
  self.global_type(global.ty)?,
@@ -155,6 +515,14 @@ impl Reencode for MeterInitializer {
155
515
  wasm_encoder::reencode::utils::parse_global(self, globals, global)?;
156
516
  }
157
517
  }
518
+ globals.global(
519
+ wasm_encoder::GlobalType {
520
+ val_type: wasm_encoder::ValType::I64,
521
+ mutable: true,
522
+ shared: false,
523
+ },
524
+ &wasm_encoder::ConstExpr::i64_const((self.budget - SAFEPOINT_INTERVAL).max(0)),
525
+ );
158
526
  Ok(())
159
527
  }
160
528
  }
@@ -162,12 +530,22 @@ impl Reencode for MeterInitializer {
162
530
  fn set_initial_meter_budget(wasm: &[u8], budget: i64) -> Result<Vec<u8>, String> {
163
531
  validate_meter_global_position(wasm)?;
164
532
  let mut module = wasm_encoder::Module::new();
165
- MeterInitializer { budget }
533
+ MeterInitializer::new(budget)
166
534
  .parse_core_module(&mut module, wasmparser::Parser::new(0), wasm)
167
535
  .map_err(|error| format!("failed to initialize weighted meter: {error}"))?;
168
536
  Ok(module.finish())
169
537
  }
170
538
 
539
+ fn has_loop(body: &wasmparser::FunctionBody<'_>) -> Result<bool, wasmparser::BinaryReaderError> {
540
+ let mut operators = body.get_operators_reader()?;
541
+ while !operators.eof() {
542
+ if matches!(operators.read()?, wasmparser::Operator::Loop { .. }) {
543
+ return Ok(true);
544
+ }
545
+ }
546
+ Ok(false)
547
+ }
548
+
171
549
  fn validate_meter_global_position(wasm: &[u8]) -> Result<(), String> {
172
550
  let mut imported_globals = 0_u32;
173
551
  let mut defined_globals = 0_u32;
@@ -271,26 +649,13 @@ fn inspect_weighted_opcodes(wasm: &[u8]) -> Result<BTreeMap<String, u64>, String
271
649
  Ok(operations)
272
650
  }
273
651
 
274
- pub fn meter_state(store: &mut impl AsStoreMut, instance: &Instance) -> Result<MeterState, String> {
275
- let gas_counter = instance
652
+ pub fn meter_state(instance: &Instance) -> Result<MeterState, String> {
653
+ instance
276
654
  .exports
277
655
  .get_global(GAS_COUNTER_NAME)
278
- .map_err(|error| format!("instrumented module does not export its meter: {error}"))?
279
- .clone();
280
-
281
- #[cfg(not(target_arch = "wasm32"))]
282
- {
283
- let _ = store;
284
- Ok(MeterState { gas_counter })
285
- }
286
-
287
- #[cfg(target_arch = "wasm32")]
288
- {
289
- let js_global: WebAssembly::Global = gas_counter.as_jsvalue(store).into();
290
- Ok(MeterState {
291
- gas_counter: js_global,
292
- })
293
- }
656
+ .cloned()
657
+ .map(|gas_counter| MeterState { gas_counter })
658
+ .map_err(|error| format!("instrumented module does not export its meter: {error}"))
294
659
  }
295
660
 
296
661
  pub fn remaining_points(
@@ -306,8 +671,8 @@ pub fn remaining_points(
306
671
 
307
672
  #[cfg(target_arch = "wasm32")]
308
673
  let value = {
309
- let _ = store;
310
- i64::try_from(BigInt::from(meter.gas_counter.value()))
674
+ let gas_counter: WebAssembly::Global = meter.gas_counter.as_jsvalue(store).into();
675
+ i64::try_from(BigInt::from(gas_counter.value()))
311
676
  .map_err(|_| "metering global is outside the signed 64-bit range".to_string())?
312
677
  };
313
678
 
@@ -318,6 +683,36 @@ pub fn remaining_points(
318
683
  }
319
684
  }
320
685
 
686
+ /// Provides `wasm_oj_metering.safepoint`, which the meter calls every `SAFEPOINT_INTERVAL` units.
687
+ /// Natively it does nothing. In a browser it calls `Atomics.wait` with a value that never matches,
688
+ /// which returns at once; JavaScriptCore checks for Worker termination there, which it never does
689
+ /// inside Wasm, so `terminate()` stops a running program within one interval in WebKit too.
690
+ pub fn attach_safepoint(store: &mut impl AsStoreMut, imports: &mut Imports) {
691
+ imports.define(
692
+ METERING_MODULE,
693
+ SAFEPOINT_NAME,
694
+ Function::new_typed(store, safepoint),
695
+ );
696
+ }
697
+
698
+ #[cfg(not(target_arch = "wasm32"))]
699
+ fn safepoint() {}
700
+
701
+ #[cfg(target_arch = "wasm32")]
702
+ fn safepoint() {
703
+ thread_local! {
704
+ static CHECKPOINT: Option<js_sys::Int32Array> = js_sys::Reflect::get(&js_sys::global(), &"SharedArrayBuffer".into())
705
+ .ok()
706
+ .filter(|constructor| constructor.is_function())
707
+ .map(|_| js_sys::Int32Array::new(&js_sys::SharedArrayBuffer::new(4)));
708
+ }
709
+ CHECKPOINT.with(|checkpoint| {
710
+ if let Some(checkpoint) = checkpoint {
711
+ let _ = js_sys::Atomics::wait_with_timeout(checkpoint, 0, 1, 0.0);
712
+ }
713
+ });
714
+ }
715
+
321
716
  fn weighted_instruction_cost(operator: &Operator) -> Option<u32> {
322
717
  let debug = format!("{operator:?}");
323
718
  weighted_opcode_cost(debug.split_whitespace().next().unwrap_or("UNKNOWN"))
@@ -453,10 +848,12 @@ fn wark_v03_opcode_cost(opcode: &str) -> u32 {
453
848
 
454
849
  #[cfg(test)]
455
850
  mod tests {
456
- use super::{METER_MODEL, instrument_wasm, weighted_opcode_cost};
851
+ use super::{METER_MODEL, SAFEPOINT_INTERVAL, instrument_wasm, weighted_opcode_cost};
852
+ use crate::{DeterminismConfig, ExecutionTermination, ResourcePolicy, RunRequest};
457
853
  use std::borrow::Cow;
854
+ use std::collections::BTreeMap;
458
855
  use wasm_encoder::{Encode, Section};
459
- use wasmparser::{ExternalKind, Parser, Payload};
856
+ use wasmparser::{ExternalKind, Parser, Payload, ValType};
460
857
 
461
858
  #[test]
462
859
  fn instrumentation_adds_the_metering_global() {
@@ -535,6 +932,48 @@ mod tests {
535
932
  assert_eq!(weighted_opcode_cost("AtomicFence"), Some(1000));
536
933
  }
537
934
 
935
+ #[test]
936
+ fn functions_with_a_loop_and_no_params_or_locals_get_one_unused_local() {
937
+ let wasm = wat::parse_str(
938
+ r#"(module
939
+ (memory (export "memory") 1)
940
+ (func (export "_start") (loop (br 0)))
941
+ (func (local i64) (loop (br 0)))
942
+ (func (param i32) (loop (br 0)))
943
+ (func nop)
944
+ (func (block (loop (br 1)))))"#,
945
+ )
946
+ .unwrap();
947
+ let metered = instrument_wasm(&wasm, 1_000_000).unwrap();
948
+ let locals = Parser::new(0)
949
+ .parse_all(&metered.wasm)
950
+ .filter_map(Result::ok)
951
+ .filter_map(|payload| match payload {
952
+ Payload::CodeSectionEntry(body) => Some(
953
+ body.get_locals_reader()
954
+ .unwrap()
955
+ .into_iter()
956
+ .map(|local| local.unwrap())
957
+ .collect::<Vec<_>>(),
958
+ ),
959
+ _ => None,
960
+ })
961
+ .collect::<Vec<_>>();
962
+ let i32_local = vec![(1, ValType::I32)];
963
+ assert_eq!(
964
+ locals,
965
+ [
966
+ i32_local.clone(),
967
+ vec![(1, ValType::I64)],
968
+ vec![],
969
+ vec![],
970
+ i32_local,
971
+ vec![],
972
+ vec![(1, ValType::I64)],
973
+ ]
974
+ );
975
+ }
976
+
538
977
  #[test]
539
978
  fn reports_wark_compatible_static_operation_counts() {
540
979
  let wasm = wat::parse_str(
@@ -546,4 +985,235 @@ mod tests {
546
985
  assert_eq!(metered.operations.get("I32Add"), Some(&1));
547
986
  assert_eq!(metered.operations.get("Drop"), Some(&1));
548
987
  }
988
+
989
+ fn looping(iterations: u32) -> Vec<u8> {
990
+ wat::parse_str(format!(
991
+ r#"(module
992
+ (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32)))
993
+ (memory (export "memory") 1)
994
+ (func (export "_start")
995
+ (local $remaining i32)
996
+ i32.const {iterations} local.set $remaining
997
+ (loop $again
998
+ local.get $remaining i32.const 1 i32.sub local.tee $remaining
999
+ br_if $again)))"#
1000
+ ))
1001
+ .unwrap()
1002
+ }
1003
+
1004
+ fn run(wasm: &[u8], instruction_budget: u64) -> crate::RunResult {
1005
+ crate::run(RunRequest {
1006
+ wasm: wasm.to_vec(),
1007
+ args: Vec::new(),
1008
+ env: BTreeMap::new(),
1009
+ stdin: Vec::new(),
1010
+ files: BTreeMap::new(),
1011
+ output_paths: Vec::new(),
1012
+ cwd: Some("/".to_string()),
1013
+ startup_entropy_bytes: 0,
1014
+ determinism: DeterminismConfig {
1015
+ random_seed: 7,
1016
+ realtime_epoch_ms: 946_684_800_000,
1017
+ clock_step_ns: 1_000_000,
1018
+ },
1019
+ resources: ResourcePolicy {
1020
+ instruction_budget,
1021
+ logical_time_limit_ms: 60_000,
1022
+ memory_limit_bytes: 64 * 1024 * 1024,
1023
+ output_limit_bytes: 1024,
1024
+ filesystem_write_limit_bytes: 64 * 1024 * 1024,
1025
+ filesystem_entry_limit: 4_096,
1026
+ },
1027
+ })
1028
+ .unwrap()
1029
+ }
1030
+
1031
+ #[test]
1032
+ fn costs_and_exhaustion_do_not_depend_on_safepoint_intervals() {
1033
+ let wasm = looping(3_000_000);
1034
+ let generous = run(&wasm, 10_000_000_000);
1035
+ assert_eq!(generous.termination, ExecutionTermination::Exited);
1036
+ let cost = generous.metrics.cost;
1037
+ assert!(cost > 5 * SAFEPOINT_INTERVAL as u64);
1038
+ assert_eq!(run(&wasm, cost * 7).metrics.cost, cost);
1039
+
1040
+ let exact = run(&wasm, cost);
1041
+ assert_eq!(exact.termination, ExecutionTermination::Exited);
1042
+ assert_eq!(exact.metrics.cost, cost);
1043
+
1044
+ let short = run(&wasm, cost - 1);
1045
+ assert_eq!(short.termination, ExecutionTermination::InstructionLimit);
1046
+ assert_eq!(short.metrics.cost, cost - 1);
1047
+
1048
+ let within_one_interval = looping(1_000);
1049
+ let small = run(&within_one_interval, 1_000_000).metrics.cost;
1050
+ assert_eq!(run(&within_one_interval, small).metrics.cost, small);
1051
+ assert_eq!(
1052
+ run(&within_one_interval, small - 1).termination,
1053
+ ExecutionTermination::InstructionLimit
1054
+ );
1055
+ }
1056
+
1057
+ #[test]
1058
+ fn the_meter_calls_the_safepoint_once_per_interval() {
1059
+ let recursing = wat::parse_str(
1060
+ r#"(module
1061
+ (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32)))
1062
+ (memory (export "memory") 1)
1063
+ (func $fib (param $n i32) (result i32)
1064
+ local.get $n i32.const 2 i32.lt_u
1065
+ if (result i32) local.get $n
1066
+ else
1067
+ local.get $n i32.const 1 i32.sub call $fib
1068
+ local.get $n i32.const 2 i32.sub call $fib
1069
+ i32.add
1070
+ end)
1071
+ (func (export "_start") i32.const 25 call $fib drop))"#,
1072
+ )
1073
+ .unwrap();
1074
+ for wasm in [looping(3_000_000), recursing] {
1075
+ let (calls, cost) = count_safepoints(&wasm);
1076
+ assert!(cost > 5 * SAFEPOINT_INTERVAL as u64);
1077
+ assert!(calls.abs_diff(cost / SAFEPOINT_INTERVAL as u64) <= 1);
1078
+ }
1079
+ }
1080
+
1081
+ #[test]
1082
+ fn safepoint_loops_keep_their_branches_and_results() {
1083
+ use wasmer::{Function, Imports, Instance, Module, Store, Value};
1084
+
1085
+ let wasm = wat::parse_str(
1086
+ r#"(module
1087
+ (memory (export "memory") 1)
1088
+ (func (export "compute") (result i32)
1089
+ (local $i i32) (local $j i32) (local $acc i32)
1090
+ (block $done
1091
+ (loop $outer
1092
+ (local.set $j (i32.const 0))
1093
+ (block $inner_done
1094
+ (loop $inner
1095
+ (local.set $acc
1096
+ (i32.add (local.get $acc) (i32.mul (local.get $i) (local.get $j))))
1097
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
1098
+ (br_if $done (i32.eq (local.get $acc) (i32.const -1)))
1099
+ (br_table $inner $inner_done $done
1100
+ (i32.gt_u (local.get $j) (i32.const 500)))))
1101
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1102
+ (br_if $outer (i32.lt_u (local.get $i) (i32.const 3000)))))
1103
+ (local.get $acc)
1104
+ (block $value (result i32)
1105
+ (loop $early (result i32) (br $value (i32.const 3))))
1106
+ (i32.add)
1107
+ (loop $fallthrough (result i32) (i32.const 7))
1108
+ (i32.add)
1109
+ (i32.const 5)
1110
+ (loop $param (param i32) (result i32)
1111
+ i32.const 1
1112
+ i32.sub
1113
+ local.tee $j
1114
+ local.get $j
1115
+ br_if $param)
1116
+ (i32.add)))"#,
1117
+ )
1118
+ .unwrap();
1119
+ let mut results = Vec::new();
1120
+ for module in [
1121
+ wasm.clone(),
1122
+ instrument_wasm(&wasm, 10_000_000_000).unwrap().wasm,
1123
+ ] {
1124
+ let mut store = Store::default();
1125
+ let module = Module::new(&store, &module).unwrap();
1126
+ let mut imports = Imports::new();
1127
+ imports.define(
1128
+ "wasm_oj_metering",
1129
+ "safepoint",
1130
+ Function::new_typed(&mut store, || {}),
1131
+ );
1132
+ let instance = Instance::new(&mut store, &module, &imports).unwrap();
1133
+ let compute = instance.exports.get_function("compute").unwrap();
1134
+ results.push(compute.call(&mut store, &[]).unwrap().to_vec());
1135
+ if let Ok(state) = super::meter_state(&instance) {
1136
+ let super::CostPoints::Remaining(remaining) =
1137
+ super::remaining_points(&mut store, &state).unwrap()
1138
+ else {
1139
+ panic!("meter exhausted");
1140
+ };
1141
+ assert!(10_000_000_000 - remaining > 5 * SAFEPOINT_INTERVAL as u64);
1142
+ }
1143
+ }
1144
+ assert_eq!(results[0], results[1]);
1145
+ assert!(matches!(results[0][..], [Value::I32(_)]));
1146
+ }
1147
+
1148
+ fn count_safepoints(wasm: &[u8]) -> (u64, u64) {
1149
+ use std::sync::Arc;
1150
+ use std::sync::atomic::{AtomicU64, Ordering};
1151
+ use wasmer::{Function, FunctionEnv, FunctionEnvMut, Imports, Instance, Module, Store};
1152
+
1153
+ let budget = 10_000_000_000;
1154
+ let metered = instrument_wasm(wasm, budget).unwrap();
1155
+ let mut store = Store::default();
1156
+ let module = Module::new(&store, &metered.wasm).unwrap();
1157
+ let calls = Arc::new(AtomicU64::new(0));
1158
+ let env = FunctionEnv::new(&mut store, calls.clone());
1159
+ let mut imports = Imports::new();
1160
+ imports.define(
1161
+ "wasm_oj_metering",
1162
+ "safepoint",
1163
+ Function::new_typed_with_env(
1164
+ &mut store,
1165
+ &env,
1166
+ |env: FunctionEnvMut<Arc<AtomicU64>>| {
1167
+ env.data().fetch_add(1, Ordering::Relaxed);
1168
+ },
1169
+ ),
1170
+ );
1171
+ imports.define(
1172
+ "wasi_snapshot_preview1",
1173
+ "proc_exit",
1174
+ Function::new_typed(&mut store, |_: i32| {}),
1175
+ );
1176
+ let instance = Instance::new(&mut store, &module, &imports).unwrap();
1177
+ instance
1178
+ .exports
1179
+ .get_function("_start")
1180
+ .unwrap()
1181
+ .call(&mut store, &[])
1182
+ .unwrap();
1183
+ let state = super::meter_state(&instance).unwrap();
1184
+ let super::CostPoints::Remaining(remaining) =
1185
+ super::remaining_points(&mut store, &state).unwrap()
1186
+ else {
1187
+ panic!("meter exhausted");
1188
+ };
1189
+ (calls.load(Ordering::Relaxed), budget - remaining)
1190
+ }
1191
+
1192
+ #[test]
1193
+ fn modules_without_imports_receive_the_safepoint_import() {
1194
+ let wasm =
1195
+ wat::parse_str("(module (memory (export \"memory\") 1) (func (export \"_start\")))")
1196
+ .unwrap();
1197
+ let metered = instrument_wasm(&wasm, 1_000_000).unwrap();
1198
+ let imports = Parser::new(0)
1199
+ .parse_all(&metered.wasm)
1200
+ .filter_map(Result::ok)
1201
+ .filter_map(|payload| match payload {
1202
+ Payload::ImportSection(section) => Some(
1203
+ section
1204
+ .into_imports()
1205
+ .map(|import| {
1206
+ let import = import.unwrap();
1207
+ format!("{}.{}", import.module, import.name)
1208
+ })
1209
+ .collect::<Vec<_>>(),
1210
+ ),
1211
+ _ => None,
1212
+ })
1213
+ .collect::<Vec<_>>();
1214
+ assert_eq!(imports, [vec!["wasm_oj_metering.safepoint".to_string()]]);
1215
+ wasmparser::Validator::new()
1216
+ .validate_all(&metered.wasm)
1217
+ .unwrap();
1218
+ }
549
1219
  }