@wasm-oj/server 0.2.3 → 0.2.4
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/deterministic.rs +27 -1
- package/crates/runtime-core/src/interactive.rs +233 -111
- package/crates/runtime-core/src/meter.rs +8 -55
- package/crates/runtime-core/src/run/mod.rs +2 -0
- package/crates/runtime-core/src/run/native.rs +1 -1
- package/crates/runtime-core/src/run/web.rs +35 -12
- package/crates/runtime-core/src/run/web_interactive.rs +370 -0
- package/crates/runtime-core/src/web.rs +52 -6
- package/dist/chunks/process-keepalive-BaXpMTap.js +191 -0
- package/dist/index.js +107 -18
- package/dist/server-build-stage.mjs +11 -94
- package/dist/server-runner-stage.mjs +73 -4
- package/package.json +3 -3
- package/dist/chunks/process-keepalive-BWLUz55N.js +0 -16
|
@@ -6,6 +6,7 @@ use crate::{
|
|
|
6
6
|
},
|
|
7
7
|
types::DeterminismConfig,
|
|
8
8
|
};
|
|
9
|
+
use std::cell::Cell;
|
|
9
10
|
use std::sync::{Arc, Mutex};
|
|
10
11
|
use wasmer::{
|
|
11
12
|
AsStoreMut, Extern, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, Memory32, Memory64,
|
|
@@ -239,6 +240,23 @@ struct PollEnv {
|
|
|
239
240
|
clock: VirtualClock,
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
thread_local! {
|
|
244
|
+
static STDIN_READINESS_DEFERRALS: Cell<u64> = const { Cell::new(0) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Whether an empty stdin readiness check may return `Pending` instead of
|
|
248
|
+
/// blocking. A clock probe never blocks. A poll without a clock grants one
|
|
249
|
+
/// deferral per read subscription, so WASIX reports any other ready
|
|
250
|
+
/// subscription first and blocks on stdin only when it would wait anyway.
|
|
251
|
+
#[cfg(target_arch = "wasm32")]
|
|
252
|
+
pub(crate) fn defer_stdin_readiness() -> bool {
|
|
253
|
+
STDIN_READINESS_DEFERRALS.with(|deferrals| {
|
|
254
|
+
let remaining = deferrals.get();
|
|
255
|
+
deferrals.set(remaining.saturating_sub(1));
|
|
256
|
+
remaining > 0
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
242
260
|
#[derive(Clone, Copy)]
|
|
243
261
|
struct ClockSubscription {
|
|
244
262
|
subscription: Subscription,
|
|
@@ -251,6 +269,7 @@ fn call_original_poll<M: MemorySize>(
|
|
|
251
269
|
events: WasmPtr<Event, M>,
|
|
252
270
|
subscription_count: M::Offset,
|
|
253
271
|
event_count: WasmPtr<M::Offset, M>,
|
|
272
|
+
stdin_deferrals: u64,
|
|
254
273
|
) -> Result<i32, RuntimeError> {
|
|
255
274
|
let Some(original) = env.data().original.clone() else {
|
|
256
275
|
return Ok(Errno::Notsup as i32);
|
|
@@ -262,6 +281,7 @@ fn call_original_poll<M: MemorySize>(
|
|
|
262
281
|
Value::I32(offset as i32)
|
|
263
282
|
}
|
|
264
283
|
};
|
|
284
|
+
STDIN_READINESS_DEFERRALS.with(|deferrals| deferrals.set(stdin_deferrals));
|
|
265
285
|
let results = original.call(
|
|
266
286
|
env,
|
|
267
287
|
&[
|
|
@@ -270,7 +290,9 @@ fn call_original_poll<M: MemorySize>(
|
|
|
270
290
|
pointer(subscription_count.into()),
|
|
271
291
|
pointer(event_count.offset().into()),
|
|
272
292
|
],
|
|
273
|
-
)
|
|
293
|
+
);
|
|
294
|
+
STDIN_READINESS_DEFERRALS.with(|deferrals| deferrals.set(0));
|
|
295
|
+
let results = results?;
|
|
274
296
|
match results.as_ref() {
|
|
275
297
|
[Value::I32(errno)] => Ok(*errno),
|
|
276
298
|
_ => Err(RuntimeError::new(
|
|
@@ -302,6 +324,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
|
|
|
302
324
|
let mut originals = Vec::with_capacity(input.len() as usize);
|
|
303
325
|
let mut clock_subscriptions = Vec::new();
|
|
304
326
|
let mut has_non_clock = false;
|
|
327
|
+
let mut read_subscriptions = 0;
|
|
305
328
|
for index in 0..input.len() {
|
|
306
329
|
let subscription = input
|
|
307
330
|
.index(index)
|
|
@@ -320,6 +343,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
|
|
|
320
343
|
});
|
|
321
344
|
} else {
|
|
322
345
|
has_non_clock = true;
|
|
346
|
+
read_subscriptions += u64::from(subscription.type_ == Eventtype::FdRead);
|
|
323
347
|
}
|
|
324
348
|
}
|
|
325
349
|
|
|
@@ -330,6 +354,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
|
|
|
330
354
|
events,
|
|
331
355
|
subscription_count,
|
|
332
356
|
event_count,
|
|
357
|
+
read_subscriptions,
|
|
333
358
|
);
|
|
334
359
|
}
|
|
335
360
|
|
|
@@ -355,6 +380,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
|
|
|
355
380
|
events,
|
|
356
381
|
subscription_count,
|
|
357
382
|
event_count,
|
|
383
|
+
u64::MAX,
|
|
358
384
|
);
|
|
359
385
|
let restore_view = memory.view(&env);
|
|
360
386
|
let restore_input = subscriptions
|
|
@@ -3,10 +3,7 @@ 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::{
|
|
7
|
-
CONTESTANT_METERING_MODULE, HOST_GAS_FUNCTION, INTERACTOR_METERING_MODULE,
|
|
8
|
-
instrument_wasm_with_host_meter,
|
|
9
|
-
};
|
|
6
|
+
use crate::meter::{CostPoints, MeterState, instrument_wasm, meter_state, remaining_points};
|
|
10
7
|
use crate::module_imports::attach_declared_memory_imports;
|
|
11
8
|
use crate::module_policy::{
|
|
12
9
|
DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit,
|
|
@@ -17,6 +14,7 @@ use crate::{
|
|
|
17
14
|
ExecutionTermination, InteractiveMetrics, InteractiveProcessResult, InteractiveProgram,
|
|
18
15
|
InteractiveRequest, InteractiveResult, RunError,
|
|
19
16
|
};
|
|
17
|
+
use futures::channel::oneshot;
|
|
20
18
|
use std::collections::{BTreeMap, HashMap};
|
|
21
19
|
use std::io;
|
|
22
20
|
use std::pin::Pin;
|
|
@@ -24,75 +22,17 @@ use std::sync::{Arc, Mutex};
|
|
|
24
22
|
use std::task::{Context, Poll};
|
|
25
23
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
|
|
26
24
|
use virtual_fs::{FsError, Pipe, PipeRx, PipeTx, VirtualFile};
|
|
27
|
-
use wasmer::{
|
|
28
|
-
AsStoreMut, Engine, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, RuntimeError,
|
|
29
|
-
};
|
|
25
|
+
use wasmer::{AsStoreMut, Engine, Imports, Memory};
|
|
30
26
|
use wasmer_types::StoreId;
|
|
31
|
-
use wasmer_wasix::bin_factory::
|
|
27
|
+
use wasmer_wasix::bin_factory::{run_exec, spawn_load_module};
|
|
28
|
+
use wasmer_wasix::os::task::TaskJoinHandle;
|
|
32
29
|
use wasmer_wasix::runtime::module_cache::{self, HashedModuleData, ModuleCache};
|
|
30
|
+
use wasmer_wasix::runtime::task_manager::TaskWasm;
|
|
33
31
|
use wasmer_wasix::{
|
|
34
32
|
PluggableRuntime, Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiVersion,
|
|
35
33
|
generate_import_object_from_env,
|
|
36
34
|
};
|
|
37
35
|
|
|
38
|
-
#[derive(Debug)]
|
|
39
|
-
struct GasBudget {
|
|
40
|
-
initial: u64,
|
|
41
|
-
state: Mutex<GasState>,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
#[derive(Debug)]
|
|
45
|
-
struct GasState {
|
|
46
|
-
remaining: u64,
|
|
47
|
-
exhausted: bool,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
impl GasBudget {
|
|
51
|
-
fn new(initial: u64) -> Self {
|
|
52
|
-
Self {
|
|
53
|
-
initial,
|
|
54
|
-
state: Mutex::new(GasState {
|
|
55
|
-
remaining: initial,
|
|
56
|
-
exhausted: false,
|
|
57
|
-
}),
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
fn charge(&self, amount: u64) -> Result<(), RuntimeError> {
|
|
62
|
-
let mut state = self
|
|
63
|
-
.state
|
|
64
|
-
.lock()
|
|
65
|
-
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
|
66
|
-
if amount > state.remaining {
|
|
67
|
-
state.remaining = 0;
|
|
68
|
-
state.exhausted = true;
|
|
69
|
-
return Err(RuntimeError::new("WASM-OJ instruction budget exhausted"));
|
|
70
|
-
}
|
|
71
|
-
state.remaining -= amount;
|
|
72
|
-
Ok(())
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
fn metrics(&self) -> Result<(u64, bool), RunError> {
|
|
76
|
-
let state = self
|
|
77
|
-
.state
|
|
78
|
-
.lock()
|
|
79
|
-
.map_err(|error| RunError::Runtime(error.to_string()))?;
|
|
80
|
-
Ok((
|
|
81
|
-
self.initial.saturating_sub(state.remaining),
|
|
82
|
-
state.exhausted,
|
|
83
|
-
))
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
#[derive(Clone)]
|
|
88
|
-
struct GasEnv(Arc<GasBudget>);
|
|
89
|
-
|
|
90
|
-
fn charge_gas(env: FunctionEnvMut<GasEnv>, amount: i64) -> Result<(), RuntimeError> {
|
|
91
|
-
let amount = u64::try_from(amount)
|
|
92
|
-
.map_err(|_| RuntimeError::new("WASM-OJ received a negative instruction charge"))?;
|
|
93
|
-
env.data().0.charge(amount)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
36
|
#[derive(Debug)]
|
|
97
37
|
struct InteractiveInput {
|
|
98
38
|
pipe: PipeRx,
|
|
@@ -263,7 +203,8 @@ impl VirtualFile for InteractiveOutput {
|
|
|
263
203
|
struct PreparedProgram {
|
|
264
204
|
wasm: Vec<u8>,
|
|
265
205
|
operations: BTreeMap<String, u64>,
|
|
266
|
-
|
|
206
|
+
budget: u64,
|
|
207
|
+
meter: Arc<Mutex<Option<MeterState>>>,
|
|
267
208
|
protocol: OutputCapture,
|
|
268
209
|
stderr: OutputCapture,
|
|
269
210
|
filesystem: RuntimeProjectFilesystem,
|
|
@@ -272,16 +213,8 @@ struct PreparedProgram {
|
|
|
272
213
|
|
|
273
214
|
pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult, RunError> {
|
|
274
215
|
validate_request(&request)?;
|
|
275
|
-
let contestant = prepare_program(
|
|
276
|
-
|
|
277
|
-
CONTESTANT_METERING_MODULE,
|
|
278
|
-
&request.determinism,
|
|
279
|
-
)?;
|
|
280
|
-
let interactor = prepare_program(
|
|
281
|
-
&request.interactor,
|
|
282
|
-
INTERACTOR_METERING_MODULE,
|
|
283
|
-
&request.determinism,
|
|
284
|
-
)?;
|
|
216
|
+
let contestant = prepare_program(&request.contestant, &request.determinism)?;
|
|
217
|
+
let interactor = prepare_program(&request.interactor, &request.determinism)?;
|
|
285
218
|
let maximum_memory = request
|
|
286
219
|
.contestant
|
|
287
220
|
.resources
|
|
@@ -293,7 +226,7 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
|
|
|
293
226
|
let contestant_runtime = interactive_runtime(
|
|
294
227
|
engine.clone(),
|
|
295
228
|
&request.determinism,
|
|
296
|
-
contestant.
|
|
229
|
+
contestant.meter.clone(),
|
|
297
230
|
contestant.clock.clone(),
|
|
298
231
|
request.contestant.startup_entropy_bytes,
|
|
299
232
|
module_cache.clone(),
|
|
@@ -303,7 +236,7 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
|
|
|
303
236
|
let interactor_runtime = interactive_runtime(
|
|
304
237
|
engine,
|
|
305
238
|
&request.determinism,
|
|
306
|
-
interactor.
|
|
239
|
+
interactor.meter.clone(),
|
|
307
240
|
interactor.clock.clone(),
|
|
308
241
|
request.interactor.startup_entropy_bytes,
|
|
309
242
|
module_cache,
|
|
@@ -334,36 +267,38 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
|
|
|
334
267
|
*interactor_wasi
|
|
335
268
|
.lock()
|
|
336
269
|
.map_err(|error| RunError::Runtime(error.to_string()))? = Some(interactor_env.clone());
|
|
337
|
-
let mut contestant_handle =
|
|
338
|
-
HashedModuleData::new(contestant.wasm.clone()),
|
|
270
|
+
let (mut contestant_handle, contestant_points) = spawn_metered(
|
|
339
271
|
"contestant",
|
|
272
|
+
&contestant,
|
|
340
273
|
contestant_env,
|
|
341
274
|
&contestant_runtime,
|
|
342
275
|
)
|
|
343
|
-
.await
|
|
344
|
-
|
|
345
|
-
let mut interactor_handle = spawn_exec_wasm(
|
|
346
|
-
HashedModuleData::new(interactor.wasm.clone()),
|
|
276
|
+
.await?;
|
|
277
|
+
let (mut interactor_handle, interactor_points) = spawn_metered(
|
|
347
278
|
"interactor",
|
|
279
|
+
&interactor,
|
|
348
280
|
interactor_env,
|
|
349
281
|
&interactor_runtime,
|
|
350
282
|
)
|
|
351
|
-
.await
|
|
352
|
-
|
|
353
|
-
let (contestant_status, interactor_status) = futures::join!(
|
|
283
|
+
.await?;
|
|
284
|
+
let (contestant_status, interactor_status, contestant_points, interactor_points) = futures::join!(
|
|
354
285
|
contestant_handle.wait_finished(),
|
|
355
286
|
interactor_handle.wait_finished(),
|
|
287
|
+
contestant_points,
|
|
288
|
+
interactor_points,
|
|
356
289
|
);
|
|
357
290
|
|
|
358
291
|
let contestant_to_interactor = contestant.protocol.bytes();
|
|
359
292
|
let interactor_to_contestant = interactor.protocol.bytes();
|
|
360
293
|
let contestant_result = process_result(
|
|
361
294
|
contestant_status,
|
|
295
|
+
contestant_points,
|
|
362
296
|
contestant,
|
|
363
297
|
contestant_to_interactor.len(),
|
|
364
298
|
)?;
|
|
365
299
|
let interactor_result = process_result(
|
|
366
300
|
interactor_status,
|
|
301
|
+
interactor_points,
|
|
367
302
|
interactor,
|
|
368
303
|
interactor_to_contestant.len(),
|
|
369
304
|
)?;
|
|
@@ -376,15 +311,56 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
|
|
|
376
311
|
})
|
|
377
312
|
}
|
|
378
313
|
|
|
314
|
+
/// `spawn_exec_wasm` without its CLOEXEC sweep, which a fresh environment does not need, plus a
|
|
315
|
+
/// recycle hook that reads the meter from the store after the process exits. WASIX reports the
|
|
316
|
+
/// exit before it recycles the store, so callers await both.
|
|
317
|
+
async fn spawn_metered(
|
|
318
|
+
name: &str,
|
|
319
|
+
program: &PreparedProgram,
|
|
320
|
+
env: WasiEnv,
|
|
321
|
+
runtime: &Arc<dyn Runtime + Send + Sync>,
|
|
322
|
+
) -> Result<
|
|
323
|
+
(
|
|
324
|
+
TaskJoinHandle,
|
|
325
|
+
oneshot::Receiver<Result<CostPoints, String>>,
|
|
326
|
+
),
|
|
327
|
+
RunError,
|
|
328
|
+
> {
|
|
329
|
+
let module = spawn_load_module(name, HashedModuleData::new(program.wasm.clone()), runtime)
|
|
330
|
+
.await
|
|
331
|
+
.map_err(|error| RunError::Compile(format!("failed to start {name}: {error}")))?;
|
|
332
|
+
let finished = env.thread.join_handle();
|
|
333
|
+
let meter = program.meter.clone();
|
|
334
|
+
let (points, received) = oneshot::channel();
|
|
335
|
+
let task = TaskWasm::new(Box::new(run_exec), env, module, true, true).with_recycle(Box::new(
|
|
336
|
+
move |mut exited| {
|
|
337
|
+
let read = meter
|
|
338
|
+
.lock()
|
|
339
|
+
.map_err(|error| error.to_string())
|
|
340
|
+
.and_then(|meter| {
|
|
341
|
+
meter
|
|
342
|
+
.clone()
|
|
343
|
+
.ok_or_else(|| "meter is unavailable".to_string())
|
|
344
|
+
})
|
|
345
|
+
.and_then(|meter| remaining_points(&mut exited.store, &meter));
|
|
346
|
+
let _ = points.send(read);
|
|
347
|
+
},
|
|
348
|
+
));
|
|
349
|
+
runtime
|
|
350
|
+
.task_manager()
|
|
351
|
+
.task_wasm(task)
|
|
352
|
+
.map_err(|error| RunError::Compile(format!("failed to start {name}: {error}")))?;
|
|
353
|
+
Ok((finished, received))
|
|
354
|
+
}
|
|
355
|
+
|
|
379
356
|
fn prepare_program(
|
|
380
357
|
program: &InteractiveProgram,
|
|
381
|
-
metering_module: &'static str,
|
|
382
358
|
determinism: &crate::DeterminismConfig,
|
|
383
359
|
) -> Result<PreparedProgram, RunError> {
|
|
384
360
|
let limited = enforce_memory_limit(&program.wasm, program.resources.memory_limit_bytes)
|
|
385
361
|
.map_err(RunError::Compile)?;
|
|
386
|
-
let metered =
|
|
387
|
-
|
|
362
|
+
let metered = instrument_wasm(&limited, program.resources.instruction_budget)
|
|
363
|
+
.map_err(RunError::Compile)?;
|
|
388
364
|
let executable = defer_start_section(&metered.wasm).map_err(RunError::Compile)?;
|
|
389
365
|
let wasm =
|
|
390
366
|
rewrite_interactive_deterministic_imports(&executable.wasm).map_err(RunError::Compile)?;
|
|
@@ -397,7 +373,8 @@ fn prepare_program(
|
|
|
397
373
|
Ok(PreparedProgram {
|
|
398
374
|
wasm,
|
|
399
375
|
operations: metered.operations,
|
|
400
|
-
|
|
376
|
+
budget: program.resources.instruction_budget,
|
|
377
|
+
meter: Arc::new(Mutex::new(None)),
|
|
401
378
|
protocol,
|
|
402
379
|
stderr,
|
|
403
380
|
filesystem,
|
|
@@ -443,12 +420,20 @@ fn build_environment(
|
|
|
443
420
|
|
|
444
421
|
fn process_result(
|
|
445
422
|
status: Result<wasmer_wasix::wasmer_wasix_types::wasi::ExitCode, Arc<WasiRuntimeError>>,
|
|
423
|
+
points: Result<Result<CostPoints, String>, oneshot::Canceled>,
|
|
446
424
|
prepared: PreparedProgram,
|
|
447
425
|
protocol_bytes: usize,
|
|
448
426
|
) -> Result<InteractiveProcessResult, RunError> {
|
|
449
427
|
let stderr = prepared.stderr.bytes();
|
|
450
428
|
let output_exceeded = prepared.protocol.exceeded() || prepared.stderr.exceeded();
|
|
451
|
-
let
|
|
429
|
+
let points = points
|
|
430
|
+
.map_err(|_| RunError::Runtime("interactive process exited without its meter".to_string()))?
|
|
431
|
+
.map_err(RunError::Runtime)?;
|
|
432
|
+
let exhausted = points == CostPoints::Exhausted;
|
|
433
|
+
let cost = match points {
|
|
434
|
+
CostPoints::Remaining(points) => prepared.budget.saturating_sub(points),
|
|
435
|
+
CostPoints::Exhausted => prepared.budget,
|
|
436
|
+
};
|
|
452
437
|
let logical_time_exceeded = prepared.clock.limit_exceeded()?;
|
|
453
438
|
let (code, termination) = if prepared.filesystem.quota_exceeded() {
|
|
454
439
|
(137, ExecutionTermination::FilesystemLimit)
|
|
@@ -516,7 +501,7 @@ fn validate_request(request: &InteractiveRequest) -> Result<(), RunError> {
|
|
|
516
501
|
fn interactive_runtime(
|
|
517
502
|
engine: Engine,
|
|
518
503
|
determinism: &crate::DeterminismConfig,
|
|
519
|
-
|
|
504
|
+
meter: Arc<Mutex<Option<MeterState>>>,
|
|
520
505
|
clock: VirtualClock,
|
|
521
506
|
startup_entropy_bytes: u64,
|
|
522
507
|
module_cache: Arc<dyn ModuleCache + Send + Sync>,
|
|
@@ -578,22 +563,6 @@ fn interactive_runtime(
|
|
|
578
563
|
clock.clone(),
|
|
579
564
|
startup_entropy_bytes,
|
|
580
565
|
);
|
|
581
|
-
let metering_module = module
|
|
582
|
-
.imports()
|
|
583
|
-
.find_map(|import| match import.module() {
|
|
584
|
-
CONTESTANT_METERING_MODULE => Some(CONTESTANT_METERING_MODULE),
|
|
585
|
-
INTERACTOR_METERING_MODULE => Some(INTERACTOR_METERING_MODULE),
|
|
586
|
-
_ => None,
|
|
587
|
-
})
|
|
588
|
-
.ok_or_else(|| {
|
|
589
|
-
io::Error::other("interactive module has no WASM-OJ metering identity")
|
|
590
|
-
})?;
|
|
591
|
-
let gas_env = FunctionEnv::new(&mut *store, GasEnv(gas.clone()));
|
|
592
|
-
imports.define(
|
|
593
|
-
metering_module,
|
|
594
|
-
HOST_GAS_FUNCTION,
|
|
595
|
-
Function::new_typed_with_env(&mut *store, &gas_env, charge_gas),
|
|
596
|
-
);
|
|
597
566
|
attach_capability_denials(store, module, &mut imports).map_err(io::Error::other)?;
|
|
598
567
|
attach_declared_memory_imports(store, module, &mut imports).map_err(io::Error::other)?;
|
|
599
568
|
Ok(imports)
|
|
@@ -616,6 +585,10 @@ fn interactive_runtime(
|
|
|
616
585
|
.memory
|
|
617
586
|
.lock()
|
|
618
587
|
.map_err(|error| io::Error::other(error.to_string()))? = Some(memory);
|
|
588
|
+
*meter
|
|
589
|
+
.lock()
|
|
590
|
+
.map_err(|error| io::Error::other(error.to_string()))? =
|
|
591
|
+
Some(meter_state(instance).map_err(io::Error::other)?);
|
|
619
592
|
pending
|
|
620
593
|
.wasi
|
|
621
594
|
.initialize(&mut *store, instance.clone())
|
|
@@ -673,7 +646,7 @@ mod tests {
|
|
|
673
646
|
use super::interact;
|
|
674
647
|
use crate::{
|
|
675
648
|
DeterminismConfig, ExecutionTermination, InteractiveProgram, InteractiveRequest,
|
|
676
|
-
ResourcePolicy,
|
|
649
|
+
ResourcePolicy, RunRequest,
|
|
677
650
|
};
|
|
678
651
|
use std::collections::BTreeMap;
|
|
679
652
|
|
|
@@ -998,6 +971,155 @@ mod tests {
|
|
|
998
971
|
assert_eq!(result.interactor.metrics.logical_time_ns, 0);
|
|
999
972
|
}
|
|
1000
973
|
|
|
974
|
+
#[test]
|
|
975
|
+
fn interactive_empty_stdin_poll_times_out_on_the_process_clock() {
|
|
976
|
+
let poller = wat::parse_str(
|
|
977
|
+
r#"(module
|
|
978
|
+
(import "wasi_snapshot_preview1" "poll_oneoff"
|
|
979
|
+
(func $poll (param i32 i32 i32 i32) (result i32)))
|
|
980
|
+
(import "wasi_snapshot_preview1" "fd_write"
|
|
981
|
+
(func $fd_write (param i32 i32 i32 i32) (result i32)))
|
|
982
|
+
(memory (export "memory") 1)
|
|
983
|
+
(func (export "_start")
|
|
984
|
+
i32.const 0 i64.const 1 i64.store
|
|
985
|
+
i32.const 8 i32.const 1 i32.store8
|
|
986
|
+
i32.const 16 i32.const 0 i32.store
|
|
987
|
+
i32.const 48 i64.const 2 i64.store
|
|
988
|
+
i32.const 56 i32.const 0 i32.store8
|
|
989
|
+
i32.const 64 i32.const 1 i32.store
|
|
990
|
+
i32.const 72 i64.const 5000000000 i64.store
|
|
991
|
+
i32.const 80 i64.const 1 i64.store
|
|
992
|
+
i32.const 88 i32.const 0 i32.store16
|
|
993
|
+
i32.const 0 i32.const 128 i32.const 2 i32.const 240 call $poll drop
|
|
994
|
+
i32.const 200 i32.const 138 i32.store
|
|
995
|
+
i32.const 204 i32.const 1 i32.store
|
|
996
|
+
i32.const 208 i32.const 240 i32.store
|
|
997
|
+
i32.const 212 i32.const 1 i32.store
|
|
998
|
+
i32.const 1 i32.const 200 i32.const 2 i32.const 216 call $fd_write drop))"#,
|
|
999
|
+
)
|
|
1000
|
+
.unwrap();
|
|
1001
|
+
let listener = wat::parse_str(
|
|
1002
|
+
r#"(module
|
|
1003
|
+
(import "wasi_snapshot_preview1" "fd_read"
|
|
1004
|
+
(func $fd_read (param i32 i32 i32 i32) (result i32)))
|
|
1005
|
+
(memory (export "memory") 1)
|
|
1006
|
+
(func (export "_start")
|
|
1007
|
+
(i32.store (i32.const 0) (i32.const 64))
|
|
1008
|
+
(i32.store (i32.const 4) (i32.const 8))
|
|
1009
|
+
(drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))))"#,
|
|
1010
|
+
)
|
|
1011
|
+
.unwrap();
|
|
1012
|
+
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
1013
|
+
.enable_all()
|
|
1014
|
+
.build()
|
|
1015
|
+
.unwrap();
|
|
1016
|
+
let result = runtime
|
|
1017
|
+
.block_on(interact(InteractiveRequest {
|
|
1018
|
+
contestant: program(poller),
|
|
1019
|
+
interactor: program(listener),
|
|
1020
|
+
determinism: DeterminismConfig {
|
|
1021
|
+
random_seed: 7,
|
|
1022
|
+
realtime_epoch_ms: 946_684_800_000,
|
|
1023
|
+
clock_step_ns: 1_000_000,
|
|
1024
|
+
},
|
|
1025
|
+
}))
|
|
1026
|
+
.unwrap();
|
|
1027
|
+
|
|
1028
|
+
assert_eq!(result.contestant_to_interactor, [0, 1]);
|
|
1029
|
+
assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
|
|
1030
|
+
assert_eq!(result.contestant.metrics.logical_time_ns, 5_000_000_000);
|
|
1031
|
+
assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
#[test]
|
|
1035
|
+
fn interactive_contestant_is_metered_like_a_standalone_run() {
|
|
1036
|
+
let looping = |ending: &str| {
|
|
1037
|
+
wat::parse_str(format!(
|
|
1038
|
+
r#"(module
|
|
1039
|
+
(import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32)))
|
|
1040
|
+
(memory (export "memory") 1)
|
|
1041
|
+
(func (export "_start")
|
|
1042
|
+
(local $remaining i32)
|
|
1043
|
+
i32.const 1000 local.set $remaining
|
|
1044
|
+
(loop $again
|
|
1045
|
+
local.get $remaining i32.const 1 i32.sub local.tee $remaining
|
|
1046
|
+
br_if $again)
|
|
1047
|
+
{ending}))"#
|
|
1048
|
+
))
|
|
1049
|
+
.unwrap()
|
|
1050
|
+
};
|
|
1051
|
+
let determinism = DeterminismConfig {
|
|
1052
|
+
random_seed: 7,
|
|
1053
|
+
realtime_epoch_ms: 946_684_800_000,
|
|
1054
|
+
clock_step_ns: 1_000_000,
|
|
1055
|
+
};
|
|
1056
|
+
let execute = |wasm: &[u8], instruction_budget: u64| {
|
|
1057
|
+
let mut contestant = program(wasm.to_vec());
|
|
1058
|
+
contestant.resources.instruction_budget = instruction_budget;
|
|
1059
|
+
let standalone = crate::run(RunRequest {
|
|
1060
|
+
wasm: contestant.wasm.clone(),
|
|
1061
|
+
args: Vec::new(),
|
|
1062
|
+
env: BTreeMap::new(),
|
|
1063
|
+
stdin: Vec::new(),
|
|
1064
|
+
files: BTreeMap::new(),
|
|
1065
|
+
output_paths: Vec::new(),
|
|
1066
|
+
cwd: contestant.cwd.clone(),
|
|
1067
|
+
startup_entropy_bytes: 0,
|
|
1068
|
+
determinism: determinism.clone(),
|
|
1069
|
+
resources: contestant.resources.clone(),
|
|
1070
|
+
})
|
|
1071
|
+
.unwrap();
|
|
1072
|
+
let interactive = tokio::runtime::Builder::new_current_thread()
|
|
1073
|
+
.enable_all()
|
|
1074
|
+
.build()
|
|
1075
|
+
.unwrap()
|
|
1076
|
+
.block_on(interact(InteractiveRequest {
|
|
1077
|
+
interactor: contestant.clone(),
|
|
1078
|
+
contestant,
|
|
1079
|
+
determinism: determinism.clone(),
|
|
1080
|
+
}))
|
|
1081
|
+
.unwrap();
|
|
1082
|
+
assert_eq!(interactive.interactor.code, interactive.contestant.code);
|
|
1083
|
+
assert_eq!(
|
|
1084
|
+
interactive.interactor.termination,
|
|
1085
|
+
interactive.contestant.termination
|
|
1086
|
+
);
|
|
1087
|
+
assert_eq!(
|
|
1088
|
+
interactive.interactor.metrics.cost,
|
|
1089
|
+
interactive.contestant.metrics.cost
|
|
1090
|
+
);
|
|
1091
|
+
(standalone, interactive.contestant)
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
for (ending, code) in [("", 0), ("i32.const 3 call $exit", 3)] {
|
|
1095
|
+
let wasm = looping(ending);
|
|
1096
|
+
let (standalone, interactive) = execute(&wasm, 1_000_000);
|
|
1097
|
+
assert_eq!(standalone.termination, ExecutionTermination::Exited);
|
|
1098
|
+
assert_eq!(standalone.code, code);
|
|
1099
|
+
assert_eq!(interactive.termination, ExecutionTermination::Exited);
|
|
1100
|
+
assert_eq!(interactive.code, code);
|
|
1101
|
+
assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
|
|
1102
|
+
|
|
1103
|
+
let budget = standalone.metrics.cost - 1;
|
|
1104
|
+
let (standalone, interactive) = execute(&wasm, budget);
|
|
1105
|
+
assert_eq!(
|
|
1106
|
+
standalone.termination,
|
|
1107
|
+
ExecutionTermination::InstructionLimit
|
|
1108
|
+
);
|
|
1109
|
+
assert_eq!(
|
|
1110
|
+
interactive.termination,
|
|
1111
|
+
ExecutionTermination::InstructionLimit
|
|
1112
|
+
);
|
|
1113
|
+
assert_eq!(interactive.code, 137);
|
|
1114
|
+
assert_eq!(interactive.metrics.cost, budget);
|
|
1115
|
+
assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let (standalone, interactive) = execute(&looping("unreachable"), 1_000_000);
|
|
1119
|
+
assert_eq!(standalone.termination, ExecutionTermination::Trap);
|
|
1120
|
+
assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1001
1123
|
fn program(wasm: Vec<u8>) -> InteractiveProgram {
|
|
1002
1124
|
InteractiveProgram {
|
|
1003
1125
|
wasm,
|
|
@@ -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, Global, 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
|
|
|
@@ -271,26 +237,13 @@ fn inspect_weighted_opcodes(wasm: &[u8]) -> Result<BTreeMap<String, u64>, String
|
|
|
271
237
|
Ok(operations)
|
|
272
238
|
}
|
|
273
239
|
|
|
274
|
-
pub fn meter_state(
|
|
275
|
-
|
|
240
|
+
pub fn meter_state(instance: &Instance) -> Result<MeterState, String> {
|
|
241
|
+
instance
|
|
276
242
|
.exports
|
|
277
243
|
.get_global(GAS_COUNTER_NAME)
|
|
278
|
-
.
|
|
279
|
-
.
|
|
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
|
-
}
|
|
244
|
+
.cloned()
|
|
245
|
+
.map(|gas_counter| MeterState { gas_counter })
|
|
246
|
+
.map_err(|error| format!("instrumented module does not export its meter: {error}"))
|
|
294
247
|
}
|
|
295
248
|
|
|
296
249
|
pub fn remaining_points(
|
|
@@ -306,8 +259,8 @@ pub fn remaining_points(
|
|
|
306
259
|
|
|
307
260
|
#[cfg(target_arch = "wasm32")]
|
|
308
261
|
let value = {
|
|
309
|
-
let
|
|
310
|
-
i64::try_from(BigInt::from(
|
|
262
|
+
let gas_counter: WebAssembly::Global = meter.gas_counter.as_jsvalue(store).into();
|
|
263
|
+
i64::try_from(BigInt::from(gas_counter.value()))
|
|
311
264
|
.map_err(|_| "metering global is outside the signed 64-bit range".to_string())?
|
|
312
265
|
};
|
|
313
266
|
|
|
@@ -15,6 +15,8 @@ mod native;
|
|
|
15
15
|
#[cfg(target_arch = "wasm32")]
|
|
16
16
|
mod web;
|
|
17
17
|
#[cfg(target_arch = "wasm32")]
|
|
18
|
+
pub(crate) mod web_interactive;
|
|
19
|
+
#[cfg(target_arch = "wasm32")]
|
|
18
20
|
pub(crate) mod web_runtime;
|
|
19
21
|
|
|
20
22
|
pub fn run(request: RunRequest) -> Result<RunResult, RunError> {
|