@wasm-oj/server 0.2.4 → 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.
- package/crates/runtime-core/src/interactive.rs +135 -2
- package/crates/runtime-core/src/meter.rs +729 -12
- package/crates/runtime-core/src/run/native.rs +4 -1
- package/crates/runtime-core/src/run/web.rs +4 -1
- package/crates/runtime-core/src/run/web_interactive.rs +6 -1
- package/dist/index.js +3 -3
- package/dist/server-build-stage.mjs +3 -3
- package/package.json +3 -3
|
@@ -3,7 +3,9 @@ use crate::deterministic::{VirtualClock, attach_interactive_deterministic_import
|
|
|
3
3
|
use crate::filesystem::{
|
|
4
4
|
RuntimeProjectFilesystem, is_normalized_guest_path, runtime_project_files,
|
|
5
5
|
};
|
|
6
|
-
use crate::meter::{
|
|
6
|
+
use crate::meter::{
|
|
7
|
+
CostPoints, MeterState, attach_safepoint, instrument_wasm, meter_state, remaining_points,
|
|
8
|
+
};
|
|
7
9
|
use crate::module_imports::attach_declared_memory_imports;
|
|
8
10
|
use crate::module_policy::{
|
|
9
11
|
DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit,
|
|
@@ -121,6 +123,9 @@ struct InteractiveOutput {
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
impl AsyncWrite for InteractiveOutput {
|
|
126
|
+
/// Once the peer has closed its stdin, for example by exiting, the pipe reports a broken
|
|
127
|
+
/// pipe. The bytes are already in the transcript, so the write succeeds and they are dropped,
|
|
128
|
+
/// as when a judge keeps draining a pipe whose reader is gone.
|
|
124
129
|
fn poll_write(
|
|
125
130
|
mut self: Pin<&mut Self>,
|
|
126
131
|
context: &mut Context<'_>,
|
|
@@ -128,7 +133,12 @@ impl AsyncWrite for InteractiveOutput {
|
|
|
128
133
|
) -> Poll<io::Result<usize>> {
|
|
129
134
|
match Pin::new(&mut self.capture).poll_write(context, buffer) {
|
|
130
135
|
Poll::Ready(Ok(written)) => {
|
|
131
|
-
Pin::new(&mut self.pipe).poll_write(context, &buffer[..written])
|
|
136
|
+
match Pin::new(&mut self.pipe).poll_write(context, &buffer[..written]) {
|
|
137
|
+
Poll::Ready(Err(error)) if error.kind() == io::ErrorKind::BrokenPipe => {
|
|
138
|
+
Poll::Ready(Ok(written))
|
|
139
|
+
}
|
|
140
|
+
result => result,
|
|
141
|
+
}
|
|
132
142
|
}
|
|
133
143
|
result => result,
|
|
134
144
|
}
|
|
@@ -563,6 +573,7 @@ fn interactive_runtime(
|
|
|
563
573
|
clock.clone(),
|
|
564
574
|
startup_entropy_bytes,
|
|
565
575
|
);
|
|
576
|
+
attach_safepoint(store, &mut imports);
|
|
566
577
|
attach_capability_denials(store, module, &mut imports).map_err(io::Error::other)?;
|
|
567
578
|
attach_declared_memory_imports(store, module, &mut imports).map_err(io::Error::other)?;
|
|
568
579
|
Ok(imports)
|
|
@@ -1120,6 +1131,128 @@ mod tests {
|
|
|
1120
1131
|
assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
|
|
1121
1132
|
}
|
|
1122
1133
|
|
|
1134
|
+
const DRAIN_STDIN: &str = r#"
|
|
1135
|
+
(func $drain_stdin (result i32)
|
|
1136
|
+
(local $total i32)
|
|
1137
|
+
(loop $again
|
|
1138
|
+
(i32.store (i32.const 0) (i32.add (i32.const 256) (local.get $total)))
|
|
1139
|
+
(i32.store (i32.const 4) (i32.const 64))
|
|
1140
|
+
(if (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8))
|
|
1141
|
+
(then (call $exit (i32.const 3))))
|
|
1142
|
+
(local.set $total (i32.add (local.get $total) (i32.load (i32.const 8))))
|
|
1143
|
+
(br_if $again (i32.load (i32.const 8))))
|
|
1144
|
+
local.get $total)"#;
|
|
1145
|
+
|
|
1146
|
+
fn peer_program(body: &str, data: &str) -> Vec<u8> {
|
|
1147
|
+
wat::parse_str(format!(
|
|
1148
|
+
r#"(module
|
|
1149
|
+
(import "wasi_snapshot_preview1" "fd_read"
|
|
1150
|
+
(func $fd_read (param i32 i32 i32 i32) (result i32)))
|
|
1151
|
+
(import "wasi_snapshot_preview1" "fd_write"
|
|
1152
|
+
(func $fd_write (param i32 i32 i32 i32) (result i32)))
|
|
1153
|
+
(import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32)))
|
|
1154
|
+
(memory (export "memory") 1)
|
|
1155
|
+
(data (i32.const 128) "{data}")
|
|
1156
|
+
{DRAIN_STDIN}
|
|
1157
|
+
(func $write (param $length i32) (result i32)
|
|
1158
|
+
(i32.store (i32.const 16) (i32.const 128))
|
|
1159
|
+
(i32.store (i32.const 20) (local.get $length))
|
|
1160
|
+
(call $fd_write (i32.const 1) (i32.const 16) (i32.const 1) (i32.const 24)))
|
|
1161
|
+
(func (export "_start") {body}))"#
|
|
1162
|
+
))
|
|
1163
|
+
.unwrap()
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
fn interact_pair(contestant: Vec<u8>, interactor: Vec<u8>) -> crate::InteractiveResult {
|
|
1167
|
+
tokio::runtime::Builder::new_current_thread()
|
|
1168
|
+
.enable_all()
|
|
1169
|
+
.build()
|
|
1170
|
+
.unwrap()
|
|
1171
|
+
.block_on(interact(InteractiveRequest {
|
|
1172
|
+
contestant: program(contestant),
|
|
1173
|
+
interactor: program(interactor),
|
|
1174
|
+
determinism: DeterminismConfig {
|
|
1175
|
+
random_seed: 7,
|
|
1176
|
+
realtime_epoch_ms: 946_684_800_000,
|
|
1177
|
+
clock_step_ns: 1_000_000,
|
|
1178
|
+
},
|
|
1179
|
+
}))
|
|
1180
|
+
.unwrap()
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
#[test]
|
|
1184
|
+
fn interactor_writes_to_an_exited_contestant_without_a_broken_pipe() {
|
|
1185
|
+
let contestant = peer_program("(drop (call $write (i32.const 2)))", "1\\n");
|
|
1186
|
+
let interactor = peer_program(
|
|
1187
|
+
r#"(local $errno i32)
|
|
1188
|
+
(drop (call $drain_stdin))
|
|
1189
|
+
(local.set $errno (call $write (i32.const 8)))
|
|
1190
|
+
(if (local.get $errno) (then (call $exit (local.get $errno))))
|
|
1191
|
+
(if (i32.ne (i32.load (i32.const 24)) (i32.const 8)) (then (call $exit (i32.const 4))))
|
|
1192
|
+
(call $exit (i32.const 42))"#,
|
|
1193
|
+
"correct\\n",
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
let result = interact_pair(contestant, interactor);
|
|
1197
|
+
|
|
1198
|
+
assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
|
|
1199
|
+
assert_eq!(result.contestant.code, 0);
|
|
1200
|
+
assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
|
|
1201
|
+
assert_eq!(result.interactor.code, 42);
|
|
1202
|
+
assert_eq!(result.contestant_to_interactor, b"1\n");
|
|
1203
|
+
assert_eq!(result.interactor_to_contestant, b"correct\n");
|
|
1204
|
+
assert_eq!(result.interactor.metrics.protocol_bytes, 8);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
#[test]
|
|
1208
|
+
fn contestant_reads_buffered_bytes_then_eof_after_the_interactor_exits() {
|
|
1209
|
+
let contestant = peer_program(
|
|
1210
|
+
r#"(local $total i32)
|
|
1211
|
+
(local.set $total (call $drain_stdin))
|
|
1212
|
+
(call $exit (select (i32.const 0) (i32.const 1)
|
|
1213
|
+
(i32.and
|
|
1214
|
+
(i32.eq (local.get $total) (i32.const 4))
|
|
1215
|
+
(i32.eq (i32.load (i32.const 256)) (i32.const 0x0a657962)))))"#,
|
|
1216
|
+
"",
|
|
1217
|
+
);
|
|
1218
|
+
let interactor = peer_program("(drop (call $write (i32.const 4)))", "bye\\n");
|
|
1219
|
+
|
|
1220
|
+
let result = interact_pair(contestant, interactor);
|
|
1221
|
+
|
|
1222
|
+
assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
|
|
1223
|
+
assert_eq!(result.interactor.code, 0);
|
|
1224
|
+
assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
|
|
1225
|
+
assert_eq!(result.contestant.code, 0);
|
|
1226
|
+
assert_eq!(result.interactor_to_contestant, b"bye\n");
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
#[test]
|
|
1230
|
+
fn contestant_writes_to_an_exited_interactor_without_a_broken_pipe() {
|
|
1231
|
+
let contestant = peer_program(
|
|
1232
|
+
r#"(local $round i32)
|
|
1233
|
+
(local $errno i32)
|
|
1234
|
+
(drop (call $drain_stdin))
|
|
1235
|
+
(loop $again
|
|
1236
|
+
(local.set $errno (call $write (i32.const 2)))
|
|
1237
|
+
(if (local.get $errno) (then (call $exit (local.get $errno))))
|
|
1238
|
+
(if (i32.ne (i32.load (i32.const 24)) (i32.const 2)) (then (call $exit (i32.const 4))))
|
|
1239
|
+
(local.set $round (i32.add (local.get $round) (i32.const 1)))
|
|
1240
|
+
(br_if $again (i32.lt_u (local.get $round) (i32.const 3))))
|
|
1241
|
+
(call $exit (i32.const 42))"#,
|
|
1242
|
+
"x\\n",
|
|
1243
|
+
);
|
|
1244
|
+
let interactor = peer_program("(drop (call $write (i32.const 4)))", "bye\\n");
|
|
1245
|
+
|
|
1246
|
+
let result = interact_pair(contestant, interactor);
|
|
1247
|
+
|
|
1248
|
+
assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
|
|
1249
|
+
assert_eq!(result.interactor.code, 0);
|
|
1250
|
+
assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
|
|
1251
|
+
assert_eq!(result.contestant.code, 42);
|
|
1252
|
+
assert_eq!(result.contestant_to_interactor, b"x\nx\nx\n");
|
|
1253
|
+
assert_eq!(result.interactor_to_contestant, b"bye\n");
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1123
1256
|
fn program(wasm: Vec<u8>) -> InteractiveProgram {
|
|
1124
1257
|
InteractiveProgram {
|
|
1125
1258
|
wasm,
|
|
@@ -9,7 +9,7 @@ use wasm_encoder::reencode::{Error as ReencodeError, Reencode};
|
|
|
9
9
|
use wasm_encoder::{Encode, Section};
|
|
10
10
|
#[cfg(target_arch = "wasm32")]
|
|
11
11
|
use wasmer::js::AsJs;
|
|
12
|
-
use wasmer::{AsStoreMut, Global, Instance};
|
|
12
|
+
use wasmer::{AsStoreMut, Function, Global, Imports, Instance};
|
|
13
13
|
|
|
14
14
|
pub const METER_MODEL: &str = "weighted";
|
|
15
15
|
const METERING_MODULE: &str = "wasm_oj_metering";
|
|
@@ -88,30 +88,424 @@ impl std::fmt::Display for MeterInitializationError {
|
|
|
88
88
|
|
|
89
89
|
impl std::error::Error for MeterInitializationError {}
|
|
90
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.
|
|
91
115
|
struct MeterInitializer {
|
|
92
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()))
|
|
93
368
|
}
|
|
94
369
|
|
|
95
370
|
impl Reencode for MeterInitializer {
|
|
96
371
|
type Error = MeterInitializationError;
|
|
97
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
|
+
|
|
98
492
|
fn parse_global_section(
|
|
99
493
|
&mut self,
|
|
100
494
|
globals: &mut wasm_encoder::GlobalSection,
|
|
101
495
|
section: wasmparser::GlobalSectionReader<'_>,
|
|
102
496
|
) -> Result<(), ReencodeError<Self::Error>> {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
|
|
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"))?;
|
|
108
502
|
for (ordinal, global) in section.into_iter().enumerate() {
|
|
109
503
|
let global = global?;
|
|
110
504
|
if u32::try_from(ordinal).ok() == Some(meter_ordinal) {
|
|
111
505
|
if global.ty.content_type != wasmparser::ValType::I64 || !global.ty.mutable {
|
|
112
|
-
return Err(
|
|
113
|
-
"instrumented meter global has an unexpected type"
|
|
114
|
-
))
|
|
506
|
+
return Err(meter_error(
|
|
507
|
+
"instrumented meter global has an unexpected type",
|
|
508
|
+
));
|
|
115
509
|
}
|
|
116
510
|
globals.global(
|
|
117
511
|
self.global_type(global.ty)?,
|
|
@@ -121,6 +515,14 @@ impl Reencode for MeterInitializer {
|
|
|
121
515
|
wasm_encoder::reencode::utils::parse_global(self, globals, global)?;
|
|
122
516
|
}
|
|
123
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
|
+
);
|
|
124
526
|
Ok(())
|
|
125
527
|
}
|
|
126
528
|
}
|
|
@@ -128,12 +530,22 @@ impl Reencode for MeterInitializer {
|
|
|
128
530
|
fn set_initial_meter_budget(wasm: &[u8], budget: i64) -> Result<Vec<u8>, String> {
|
|
129
531
|
validate_meter_global_position(wasm)?;
|
|
130
532
|
let mut module = wasm_encoder::Module::new();
|
|
131
|
-
MeterInitializer
|
|
533
|
+
MeterInitializer::new(budget)
|
|
132
534
|
.parse_core_module(&mut module, wasmparser::Parser::new(0), wasm)
|
|
133
535
|
.map_err(|error| format!("failed to initialize weighted meter: {error}"))?;
|
|
134
536
|
Ok(module.finish())
|
|
135
537
|
}
|
|
136
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
|
+
|
|
137
549
|
fn validate_meter_global_position(wasm: &[u8]) -> Result<(), String> {
|
|
138
550
|
let mut imported_globals = 0_u32;
|
|
139
551
|
let mut defined_globals = 0_u32;
|
|
@@ -271,6 +683,36 @@ pub fn remaining_points(
|
|
|
271
683
|
}
|
|
272
684
|
}
|
|
273
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
|
+
|
|
274
716
|
fn weighted_instruction_cost(operator: &Operator) -> Option<u32> {
|
|
275
717
|
let debug = format!("{operator:?}");
|
|
276
718
|
weighted_opcode_cost(debug.split_whitespace().next().unwrap_or("UNKNOWN"))
|
|
@@ -406,10 +848,12 @@ fn wark_v03_opcode_cost(opcode: &str) -> u32 {
|
|
|
406
848
|
|
|
407
849
|
#[cfg(test)]
|
|
408
850
|
mod tests {
|
|
409
|
-
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};
|
|
410
853
|
use std::borrow::Cow;
|
|
854
|
+
use std::collections::BTreeMap;
|
|
411
855
|
use wasm_encoder::{Encode, Section};
|
|
412
|
-
use wasmparser::{ExternalKind, Parser, Payload};
|
|
856
|
+
use wasmparser::{ExternalKind, Parser, Payload, ValType};
|
|
413
857
|
|
|
414
858
|
#[test]
|
|
415
859
|
fn instrumentation_adds_the_metering_global() {
|
|
@@ -488,6 +932,48 @@ mod tests {
|
|
|
488
932
|
assert_eq!(weighted_opcode_cost("AtomicFence"), Some(1000));
|
|
489
933
|
}
|
|
490
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
|
+
|
|
491
977
|
#[test]
|
|
492
978
|
fn reports_wark_compatible_static_operation_counts() {
|
|
493
979
|
let wasm = wat::parse_str(
|
|
@@ -499,4 +985,235 @@ mod tests {
|
|
|
499
985
|
assert_eq!(metered.operations.get("I32Add"), Some(&1));
|
|
500
986
|
assert_eq!(metered.operations.get("Drop"), Some(&1));
|
|
501
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
|
+
}
|
|
502
1219
|
}
|
|
@@ -2,7 +2,9 @@ use crate::capabilities::attach_capability_denials;
|
|
|
2
2
|
use crate::deterministic::{VirtualClock, attach_deterministic_imports};
|
|
3
3
|
use crate::filesystem::{read_files_bounded, runtime_project_files};
|
|
4
4
|
use crate::memory::LimitingTunables;
|
|
5
|
-
use crate::meter::{
|
|
5
|
+
use crate::meter::{
|
|
6
|
+
CostPoints, METER_MODEL, attach_safepoint, instrument_wasm, meter_state, remaining_points,
|
|
7
|
+
};
|
|
6
8
|
use crate::module_imports::attach_imported_memory;
|
|
7
9
|
use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
|
|
8
10
|
use crate::output::{OutputBudget, OutputCapture};
|
|
@@ -105,6 +107,7 @@ fn run_in_runtime(request: RunRequest) -> Result<RunResult, RunError> {
|
|
|
105
107
|
clock.clone(),
|
|
106
108
|
request.startup_entropy_bytes,
|
|
107
109
|
);
|
|
110
|
+
attach_safepoint(&mut store, &mut imports);
|
|
108
111
|
attach_capability_denials(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
|
|
109
112
|
let imported_memory =
|
|
110
113
|
attach_imported_memory(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
|
|
@@ -2,7 +2,9 @@ use super::web_runtime::runtime_with_engine;
|
|
|
2
2
|
use crate::capabilities::attach_capability_denials;
|
|
3
3
|
use crate::deterministic::{VirtualClock, attach_deterministic_imports};
|
|
4
4
|
use crate::filesystem::{read_files_bounded, runtime_project_files};
|
|
5
|
-
use crate::meter::{
|
|
5
|
+
use crate::meter::{
|
|
6
|
+
CostPoints, METER_MODEL, attach_safepoint, instrument_wasm, meter_state, remaining_points,
|
|
7
|
+
};
|
|
6
8
|
use crate::module_imports::attach_imported_memory;
|
|
7
9
|
use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
|
|
8
10
|
use crate::output::{CappedOutput, OutputBudget, OutputCapture};
|
|
@@ -102,6 +104,7 @@ pub(super) fn execute(
|
|
|
102
104
|
clock.clone(),
|
|
103
105
|
request.startup_entropy_bytes,
|
|
104
106
|
);
|
|
107
|
+
attach_safepoint(&mut store, &mut imports);
|
|
105
108
|
attach_capability_denials(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
|
|
106
109
|
let imported_memory =
|
|
107
110
|
attach_imported_memory(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
|
|
@@ -291,13 +291,18 @@ impl std::fmt::Debug for StreamOutput {
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
impl AsyncWrite for StreamOutput {
|
|
294
|
+
/// Drops bytes written after the peer closed its stdin, as native does; they are already in
|
|
295
|
+
/// the transcript.
|
|
294
296
|
fn poll_write(
|
|
295
297
|
mut self: Pin<&mut Self>,
|
|
296
298
|
context: &mut Context<'_>,
|
|
297
299
|
buffer: &[u8],
|
|
298
300
|
) -> Poll<io::Result<usize>> {
|
|
299
301
|
match Pin::new(&mut self.capture).poll_write(context, buffer) {
|
|
300
|
-
Poll::Ready(Ok(written)) => Poll::Ready(self.streams.write(&buffer[..written])
|
|
302
|
+
Poll::Ready(Ok(written)) => Poll::Ready(match self.streams.write(&buffer[..written]) {
|
|
303
|
+
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(written),
|
|
304
|
+
result => result,
|
|
305
|
+
}),
|
|
301
306
|
result => result,
|
|
302
307
|
}
|
|
303
308
|
}
|
package/dist/index.js
CHANGED
|
@@ -775,8 +775,8 @@ Object.freeze({
|
|
|
775
775
|
});
|
|
776
776
|
new TextEncoder();
|
|
777
777
|
Object.freeze({
|
|
778
|
-
runtimeCoreWasmSha256: "
|
|
779
|
-
runtimeSourceRootSha256: "
|
|
778
|
+
runtimeCoreWasmSha256: "077aa936a3382a242eee7222fc17369769cb5ad265303d730ffc45ee192367e9",
|
|
779
|
+
runtimeSourceRootSha256: "c65b3bbcad41359f86b8f1d1315af8f079b44f0e9084ace0d6d50c0ec418415b",
|
|
780
780
|
wasmerNativeVersion: "7.2.1",
|
|
781
781
|
wasmerSdkVersion: "0.10.0",
|
|
782
782
|
wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e",
|
|
@@ -787,7 +787,7 @@ Object.freeze({
|
|
|
787
787
|
* Release verification independently checks the component bytes before this
|
|
788
788
|
* identity is admitted into a calibrated release.
|
|
789
789
|
*/
|
|
790
|
-
var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "
|
|
790
|
+
var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "47684bee57aafd163b18a787fa16eed33cab3ec3c0005c3c46bb4f1803ffc835";
|
|
791
791
|
//#endregion
|
|
792
792
|
//#region src/core/cost-profile.ts
|
|
793
793
|
function coordinates(language, target, optimization) {
|
|
@@ -482,8 +482,8 @@ Object.freeze({
|
|
|
482
482
|
});
|
|
483
483
|
new TextEncoder();
|
|
484
484
|
Object.freeze({
|
|
485
|
-
runtimeCoreWasmSha256: "
|
|
486
|
-
runtimeSourceRootSha256: "
|
|
485
|
+
runtimeCoreWasmSha256: "077aa936a3382a242eee7222fc17369769cb5ad265303d730ffc45ee192367e9",
|
|
486
|
+
runtimeSourceRootSha256: "c65b3bbcad41359f86b8f1d1315af8f079b44f0e9084ace0d6d50c0ec418415b",
|
|
487
487
|
wasmerNativeVersion: "7.2.1",
|
|
488
488
|
wasmerSdkVersion: "0.10.0",
|
|
489
489
|
wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e",
|
|
@@ -494,7 +494,7 @@ Object.freeze({
|
|
|
494
494
|
* Release verification independently checks the component bytes before this
|
|
495
495
|
* identity is admitted into a calibrated release.
|
|
496
496
|
*/
|
|
497
|
-
var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "
|
|
497
|
+
var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "47684bee57aafd163b18a787fa16eed33cab3ec3c0005c3c46bb4f1803ffc835";
|
|
498
498
|
//#endregion
|
|
499
499
|
//#region src/core/cost-profile.ts
|
|
500
500
|
function coordinates(language, target, optimization) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wasm-oj/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Node.js compiler and runner host for the WASM-OJ SDK.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "JacobLinCool",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"node": ">=24.18.0 <25"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@wasm-oj/contracts": "0.2.
|
|
62
|
-
"@wasm-oj/core": "0.2.
|
|
61
|
+
"@wasm-oj/contracts": "0.2.5",
|
|
62
|
+
"@wasm-oj/core": "0.2.5",
|
|
63
63
|
"@wasmer/sdk": "0.10.0",
|
|
64
64
|
"fflate": "0.8.3"
|
|
65
65
|
},
|