@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.
@@ -4,8 +4,7 @@ use crate::filesystem::{
4
4
  RuntimeProjectFilesystem, is_normalized_guest_path, runtime_project_files,
5
5
  };
6
6
  use crate::meter::{
7
- CONTESTANT_METERING_MODULE, HOST_GAS_FUNCTION, INTERACTOR_METERING_MODULE,
8
- instrument_wasm_with_host_meter,
7
+ CostPoints, MeterState, attach_safepoint, instrument_wasm, meter_state, remaining_points,
9
8
  };
10
9
  use crate::module_imports::attach_declared_memory_imports;
11
10
  use crate::module_policy::{
@@ -17,6 +16,7 @@ use crate::{
17
16
  ExecutionTermination, InteractiveMetrics, InteractiveProcessResult, InteractiveProgram,
18
17
  InteractiveRequest, InteractiveResult, RunError,
19
18
  };
19
+ use futures::channel::oneshot;
20
20
  use std::collections::{BTreeMap, HashMap};
21
21
  use std::io;
22
22
  use std::pin::Pin;
@@ -24,75 +24,17 @@ use std::sync::{Arc, Mutex};
24
24
  use std::task::{Context, Poll};
25
25
  use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
26
26
  use virtual_fs::{FsError, Pipe, PipeRx, PipeTx, VirtualFile};
27
- use wasmer::{
28
- AsStoreMut, Engine, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, RuntimeError,
29
- };
27
+ use wasmer::{AsStoreMut, Engine, Imports, Memory};
30
28
  use wasmer_types::StoreId;
31
- use wasmer_wasix::bin_factory::spawn_exec_wasm;
29
+ use wasmer_wasix::bin_factory::{run_exec, spawn_load_module};
30
+ use wasmer_wasix::os::task::TaskJoinHandle;
32
31
  use wasmer_wasix::runtime::module_cache::{self, HashedModuleData, ModuleCache};
32
+ use wasmer_wasix::runtime::task_manager::TaskWasm;
33
33
  use wasmer_wasix::{
34
34
  PluggableRuntime, Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiVersion,
35
35
  generate_import_object_from_env,
36
36
  };
37
37
 
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
38
  #[derive(Debug)]
97
39
  struct InteractiveInput {
98
40
  pipe: PipeRx,
@@ -181,6 +123,9 @@ struct InteractiveOutput {
181
123
  }
182
124
 
183
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.
184
129
  fn poll_write(
185
130
  mut self: Pin<&mut Self>,
186
131
  context: &mut Context<'_>,
@@ -188,7 +133,12 @@ impl AsyncWrite for InteractiveOutput {
188
133
  ) -> Poll<io::Result<usize>> {
189
134
  match Pin::new(&mut self.capture).poll_write(context, buffer) {
190
135
  Poll::Ready(Ok(written)) => {
191
- 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
+ }
192
142
  }
193
143
  result => result,
194
144
  }
@@ -263,7 +213,8 @@ impl VirtualFile for InteractiveOutput {
263
213
  struct PreparedProgram {
264
214
  wasm: Vec<u8>,
265
215
  operations: BTreeMap<String, u64>,
266
- gas: Arc<GasBudget>,
216
+ budget: u64,
217
+ meter: Arc<Mutex<Option<MeterState>>>,
267
218
  protocol: OutputCapture,
268
219
  stderr: OutputCapture,
269
220
  filesystem: RuntimeProjectFilesystem,
@@ -272,16 +223,8 @@ struct PreparedProgram {
272
223
 
273
224
  pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult, RunError> {
274
225
  validate_request(&request)?;
275
- let contestant = prepare_program(
276
- &request.contestant,
277
- CONTESTANT_METERING_MODULE,
278
- &request.determinism,
279
- )?;
280
- let interactor = prepare_program(
281
- &request.interactor,
282
- INTERACTOR_METERING_MODULE,
283
- &request.determinism,
284
- )?;
226
+ let contestant = prepare_program(&request.contestant, &request.determinism)?;
227
+ let interactor = prepare_program(&request.interactor, &request.determinism)?;
285
228
  let maximum_memory = request
286
229
  .contestant
287
230
  .resources
@@ -293,7 +236,7 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
293
236
  let contestant_runtime = interactive_runtime(
294
237
  engine.clone(),
295
238
  &request.determinism,
296
- contestant.gas.clone(),
239
+ contestant.meter.clone(),
297
240
  contestant.clock.clone(),
298
241
  request.contestant.startup_entropy_bytes,
299
242
  module_cache.clone(),
@@ -303,7 +246,7 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
303
246
  let interactor_runtime = interactive_runtime(
304
247
  engine,
305
248
  &request.determinism,
306
- interactor.gas.clone(),
249
+ interactor.meter.clone(),
307
250
  interactor.clock.clone(),
308
251
  request.interactor.startup_entropy_bytes,
309
252
  module_cache,
@@ -334,36 +277,38 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
334
277
  *interactor_wasi
335
278
  .lock()
336
279
  .map_err(|error| RunError::Runtime(error.to_string()))? = Some(interactor_env.clone());
337
- let mut contestant_handle = spawn_exec_wasm(
338
- HashedModuleData::new(contestant.wasm.clone()),
280
+ let (mut contestant_handle, contestant_points) = spawn_metered(
339
281
  "contestant",
282
+ &contestant,
340
283
  contestant_env,
341
284
  &contestant_runtime,
342
285
  )
343
- .await
344
- .map_err(|error| RunError::Compile(format!("failed to start contestant: {error}")))?;
345
- let mut interactor_handle = spawn_exec_wasm(
346
- HashedModuleData::new(interactor.wasm.clone()),
286
+ .await?;
287
+ let (mut interactor_handle, interactor_points) = spawn_metered(
347
288
  "interactor",
289
+ &interactor,
348
290
  interactor_env,
349
291
  &interactor_runtime,
350
292
  )
351
- .await
352
- .map_err(|error| RunError::Compile(format!("failed to start interactor: {error}")))?;
353
- let (contestant_status, interactor_status) = futures::join!(
293
+ .await?;
294
+ let (contestant_status, interactor_status, contestant_points, interactor_points) = futures::join!(
354
295
  contestant_handle.wait_finished(),
355
296
  interactor_handle.wait_finished(),
297
+ contestant_points,
298
+ interactor_points,
356
299
  );
357
300
 
358
301
  let contestant_to_interactor = contestant.protocol.bytes();
359
302
  let interactor_to_contestant = interactor.protocol.bytes();
360
303
  let contestant_result = process_result(
361
304
  contestant_status,
305
+ contestant_points,
362
306
  contestant,
363
307
  contestant_to_interactor.len(),
364
308
  )?;
365
309
  let interactor_result = process_result(
366
310
  interactor_status,
311
+ interactor_points,
367
312
  interactor,
368
313
  interactor_to_contestant.len(),
369
314
  )?;
@@ -376,15 +321,56 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
376
321
  })
377
322
  }
378
323
 
324
+ /// `spawn_exec_wasm` without its CLOEXEC sweep, which a fresh environment does not need, plus a
325
+ /// recycle hook that reads the meter from the store after the process exits. WASIX reports the
326
+ /// exit before it recycles the store, so callers await both.
327
+ async fn spawn_metered(
328
+ name: &str,
329
+ program: &PreparedProgram,
330
+ env: WasiEnv,
331
+ runtime: &Arc<dyn Runtime + Send + Sync>,
332
+ ) -> Result<
333
+ (
334
+ TaskJoinHandle,
335
+ oneshot::Receiver<Result<CostPoints, String>>,
336
+ ),
337
+ RunError,
338
+ > {
339
+ let module = spawn_load_module(name, HashedModuleData::new(program.wasm.clone()), runtime)
340
+ .await
341
+ .map_err(|error| RunError::Compile(format!("failed to start {name}: {error}")))?;
342
+ let finished = env.thread.join_handle();
343
+ let meter = program.meter.clone();
344
+ let (points, received) = oneshot::channel();
345
+ let task = TaskWasm::new(Box::new(run_exec), env, module, true, true).with_recycle(Box::new(
346
+ move |mut exited| {
347
+ let read = meter
348
+ .lock()
349
+ .map_err(|error| error.to_string())
350
+ .and_then(|meter| {
351
+ meter
352
+ .clone()
353
+ .ok_or_else(|| "meter is unavailable".to_string())
354
+ })
355
+ .and_then(|meter| remaining_points(&mut exited.store, &meter));
356
+ let _ = points.send(read);
357
+ },
358
+ ));
359
+ runtime
360
+ .task_manager()
361
+ .task_wasm(task)
362
+ .map_err(|error| RunError::Compile(format!("failed to start {name}: {error}")))?;
363
+ Ok((finished, received))
364
+ }
365
+
379
366
  fn prepare_program(
380
367
  program: &InteractiveProgram,
381
- metering_module: &'static str,
382
368
  determinism: &crate::DeterminismConfig,
383
369
  ) -> Result<PreparedProgram, RunError> {
384
370
  let limited = enforce_memory_limit(&program.wasm, program.resources.memory_limit_bytes)
385
371
  .map_err(RunError::Compile)?;
386
- let metered =
387
- instrument_wasm_with_host_meter(&limited, metering_module).map_err(RunError::Compile)?;
372
+ let metered = instrument_wasm(&limited, program.resources.instruction_budget)
373
+ .map_err(RunError::Compile)?;
388
374
  let executable = defer_start_section(&metered.wasm).map_err(RunError::Compile)?;
389
375
  let wasm =
390
376
  rewrite_interactive_deterministic_imports(&executable.wasm).map_err(RunError::Compile)?;
@@ -397,7 +383,8 @@ fn prepare_program(
397
383
  Ok(PreparedProgram {
398
384
  wasm,
399
385
  operations: metered.operations,
400
- gas: Arc::new(GasBudget::new(program.resources.instruction_budget)),
386
+ budget: program.resources.instruction_budget,
387
+ meter: Arc::new(Mutex::new(None)),
401
388
  protocol,
402
389
  stderr,
403
390
  filesystem,
@@ -443,12 +430,20 @@ fn build_environment(
443
430
 
444
431
  fn process_result(
445
432
  status: Result<wasmer_wasix::wasmer_wasix_types::wasi::ExitCode, Arc<WasiRuntimeError>>,
433
+ points: Result<Result<CostPoints, String>, oneshot::Canceled>,
446
434
  prepared: PreparedProgram,
447
435
  protocol_bytes: usize,
448
436
  ) -> Result<InteractiveProcessResult, RunError> {
449
437
  let stderr = prepared.stderr.bytes();
450
438
  let output_exceeded = prepared.protocol.exceeded() || prepared.stderr.exceeded();
451
- let (cost, exhausted) = prepared.gas.metrics()?;
439
+ let points = points
440
+ .map_err(|_| RunError::Runtime("interactive process exited without its meter".to_string()))?
441
+ .map_err(RunError::Runtime)?;
442
+ let exhausted = points == CostPoints::Exhausted;
443
+ let cost = match points {
444
+ CostPoints::Remaining(points) => prepared.budget.saturating_sub(points),
445
+ CostPoints::Exhausted => prepared.budget,
446
+ };
452
447
  let logical_time_exceeded = prepared.clock.limit_exceeded()?;
453
448
  let (code, termination) = if prepared.filesystem.quota_exceeded() {
454
449
  (137, ExecutionTermination::FilesystemLimit)
@@ -516,7 +511,7 @@ fn validate_request(request: &InteractiveRequest) -> Result<(), RunError> {
516
511
  fn interactive_runtime(
517
512
  engine: Engine,
518
513
  determinism: &crate::DeterminismConfig,
519
- gas: Arc<GasBudget>,
514
+ meter: Arc<Mutex<Option<MeterState>>>,
520
515
  clock: VirtualClock,
521
516
  startup_entropy_bytes: u64,
522
517
  module_cache: Arc<dyn ModuleCache + Send + Sync>,
@@ -578,22 +573,7 @@ fn interactive_runtime(
578
573
  clock.clone(),
579
574
  startup_entropy_bytes,
580
575
  );
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
- );
576
+ attach_safepoint(store, &mut imports);
597
577
  attach_capability_denials(store, module, &mut imports).map_err(io::Error::other)?;
598
578
  attach_declared_memory_imports(store, module, &mut imports).map_err(io::Error::other)?;
599
579
  Ok(imports)
@@ -616,6 +596,10 @@ fn interactive_runtime(
616
596
  .memory
617
597
  .lock()
618
598
  .map_err(|error| io::Error::other(error.to_string()))? = Some(memory);
599
+ *meter
600
+ .lock()
601
+ .map_err(|error| io::Error::other(error.to_string()))? =
602
+ Some(meter_state(instance).map_err(io::Error::other)?);
619
603
  pending
620
604
  .wasi
621
605
  .initialize(&mut *store, instance.clone())
@@ -673,7 +657,7 @@ mod tests {
673
657
  use super::interact;
674
658
  use crate::{
675
659
  DeterminismConfig, ExecutionTermination, InteractiveProgram, InteractiveRequest,
676
- ResourcePolicy,
660
+ ResourcePolicy, RunRequest,
677
661
  };
678
662
  use std::collections::BTreeMap;
679
663
 
@@ -998,6 +982,277 @@ mod tests {
998
982
  assert_eq!(result.interactor.metrics.logical_time_ns, 0);
999
983
  }
1000
984
 
985
+ #[test]
986
+ fn interactive_empty_stdin_poll_times_out_on_the_process_clock() {
987
+ let poller = wat::parse_str(
988
+ r#"(module
989
+ (import "wasi_snapshot_preview1" "poll_oneoff"
990
+ (func $poll (param i32 i32 i32 i32) (result i32)))
991
+ (import "wasi_snapshot_preview1" "fd_write"
992
+ (func $fd_write (param i32 i32 i32 i32) (result i32)))
993
+ (memory (export "memory") 1)
994
+ (func (export "_start")
995
+ i32.const 0 i64.const 1 i64.store
996
+ i32.const 8 i32.const 1 i32.store8
997
+ i32.const 16 i32.const 0 i32.store
998
+ i32.const 48 i64.const 2 i64.store
999
+ i32.const 56 i32.const 0 i32.store8
1000
+ i32.const 64 i32.const 1 i32.store
1001
+ i32.const 72 i64.const 5000000000 i64.store
1002
+ i32.const 80 i64.const 1 i64.store
1003
+ i32.const 88 i32.const 0 i32.store16
1004
+ i32.const 0 i32.const 128 i32.const 2 i32.const 240 call $poll drop
1005
+ i32.const 200 i32.const 138 i32.store
1006
+ i32.const 204 i32.const 1 i32.store
1007
+ i32.const 208 i32.const 240 i32.store
1008
+ i32.const 212 i32.const 1 i32.store
1009
+ i32.const 1 i32.const 200 i32.const 2 i32.const 216 call $fd_write drop))"#,
1010
+ )
1011
+ .unwrap();
1012
+ let listener = wat::parse_str(
1013
+ r#"(module
1014
+ (import "wasi_snapshot_preview1" "fd_read"
1015
+ (func $fd_read (param i32 i32 i32 i32) (result i32)))
1016
+ (memory (export "memory") 1)
1017
+ (func (export "_start")
1018
+ (i32.store (i32.const 0) (i32.const 64))
1019
+ (i32.store (i32.const 4) (i32.const 8))
1020
+ (drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))))"#,
1021
+ )
1022
+ .unwrap();
1023
+ let runtime = tokio::runtime::Builder::new_current_thread()
1024
+ .enable_all()
1025
+ .build()
1026
+ .unwrap();
1027
+ let result = runtime
1028
+ .block_on(interact(InteractiveRequest {
1029
+ contestant: program(poller),
1030
+ interactor: program(listener),
1031
+ determinism: DeterminismConfig {
1032
+ random_seed: 7,
1033
+ realtime_epoch_ms: 946_684_800_000,
1034
+ clock_step_ns: 1_000_000,
1035
+ },
1036
+ }))
1037
+ .unwrap();
1038
+
1039
+ assert_eq!(result.contestant_to_interactor, [0, 1]);
1040
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
1041
+ assert_eq!(result.contestant.metrics.logical_time_ns, 5_000_000_000);
1042
+ assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
1043
+ }
1044
+
1045
+ #[test]
1046
+ fn interactive_contestant_is_metered_like_a_standalone_run() {
1047
+ let looping = |ending: &str| {
1048
+ wat::parse_str(format!(
1049
+ r#"(module
1050
+ (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32)))
1051
+ (memory (export "memory") 1)
1052
+ (func (export "_start")
1053
+ (local $remaining i32)
1054
+ i32.const 1000 local.set $remaining
1055
+ (loop $again
1056
+ local.get $remaining i32.const 1 i32.sub local.tee $remaining
1057
+ br_if $again)
1058
+ {ending}))"#
1059
+ ))
1060
+ .unwrap()
1061
+ };
1062
+ let determinism = DeterminismConfig {
1063
+ random_seed: 7,
1064
+ realtime_epoch_ms: 946_684_800_000,
1065
+ clock_step_ns: 1_000_000,
1066
+ };
1067
+ let execute = |wasm: &[u8], instruction_budget: u64| {
1068
+ let mut contestant = program(wasm.to_vec());
1069
+ contestant.resources.instruction_budget = instruction_budget;
1070
+ let standalone = crate::run(RunRequest {
1071
+ wasm: contestant.wasm.clone(),
1072
+ args: Vec::new(),
1073
+ env: BTreeMap::new(),
1074
+ stdin: Vec::new(),
1075
+ files: BTreeMap::new(),
1076
+ output_paths: Vec::new(),
1077
+ cwd: contestant.cwd.clone(),
1078
+ startup_entropy_bytes: 0,
1079
+ determinism: determinism.clone(),
1080
+ resources: contestant.resources.clone(),
1081
+ })
1082
+ .unwrap();
1083
+ let interactive = tokio::runtime::Builder::new_current_thread()
1084
+ .enable_all()
1085
+ .build()
1086
+ .unwrap()
1087
+ .block_on(interact(InteractiveRequest {
1088
+ interactor: contestant.clone(),
1089
+ contestant,
1090
+ determinism: determinism.clone(),
1091
+ }))
1092
+ .unwrap();
1093
+ assert_eq!(interactive.interactor.code, interactive.contestant.code);
1094
+ assert_eq!(
1095
+ interactive.interactor.termination,
1096
+ interactive.contestant.termination
1097
+ );
1098
+ assert_eq!(
1099
+ interactive.interactor.metrics.cost,
1100
+ interactive.contestant.metrics.cost
1101
+ );
1102
+ (standalone, interactive.contestant)
1103
+ };
1104
+
1105
+ for (ending, code) in [("", 0), ("i32.const 3 call $exit", 3)] {
1106
+ let wasm = looping(ending);
1107
+ let (standalone, interactive) = execute(&wasm, 1_000_000);
1108
+ assert_eq!(standalone.termination, ExecutionTermination::Exited);
1109
+ assert_eq!(standalone.code, code);
1110
+ assert_eq!(interactive.termination, ExecutionTermination::Exited);
1111
+ assert_eq!(interactive.code, code);
1112
+ assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
1113
+
1114
+ let budget = standalone.metrics.cost - 1;
1115
+ let (standalone, interactive) = execute(&wasm, budget);
1116
+ assert_eq!(
1117
+ standalone.termination,
1118
+ ExecutionTermination::InstructionLimit
1119
+ );
1120
+ assert_eq!(
1121
+ interactive.termination,
1122
+ ExecutionTermination::InstructionLimit
1123
+ );
1124
+ assert_eq!(interactive.code, 137);
1125
+ assert_eq!(interactive.metrics.cost, budget);
1126
+ assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
1127
+ }
1128
+
1129
+ let (standalone, interactive) = execute(&looping("unreachable"), 1_000_000);
1130
+ assert_eq!(standalone.termination, ExecutionTermination::Trap);
1131
+ assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
1132
+ }
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
+
1001
1256
  fn program(wasm: Vec<u8>) -> InteractiveProgram {
1002
1257
  InteractiveProgram {
1003
1258
  wasm,