@wasm-oj/server 0.2.0

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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/THIRD_PARTY_NOTICES.md +302 -0
  4. package/crates/runtime-core/Cargo.lock +5099 -0
  5. package/crates/runtime-core/Cargo.toml +66 -0
  6. package/crates/runtime-core/README.md +47 -0
  7. package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
  8. package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
  9. package/crates/runtime-core/src/capabilities.rs +118 -0
  10. package/crates/runtime-core/src/compiler.rs +658 -0
  11. package/crates/runtime-core/src/contract.rs +5 -0
  12. package/crates/runtime-core/src/deterministic.rs +1051 -0
  13. package/crates/runtime-core/src/error.rs +58 -0
  14. package/crates/runtime-core/src/filesystem.rs +547 -0
  15. package/crates/runtime-core/src/filesystem_quota.rs +167 -0
  16. package/crates/runtime-core/src/go_compiler_session.rs +297 -0
  17. package/crates/runtime-core/src/interactive.rs +1019 -0
  18. package/crates/runtime-core/src/judge_package.rs +1539 -0
  19. package/crates/runtime-core/src/lib.rs +98 -0
  20. package/crates/runtime-core/src/memory.rs +84 -0
  21. package/crates/runtime-core/src/meter.rs +549 -0
  22. package/crates/runtime-core/src/module_imports.rs +149 -0
  23. package/crates/runtime-core/src/module_policy.rs +714 -0
  24. package/crates/runtime-core/src/output.rs +204 -0
  25. package/crates/runtime-core/src/run/mod.rs +208 -0
  26. package/crates/runtime-core/src/run/native.rs +260 -0
  27. package/crates/runtime-core/src/run/web.rs +229 -0
  28. package/crates/runtime-core/src/run/web_runtime.rs +109 -0
  29. package/crates/runtime-core/src/types.rs +268 -0
  30. package/crates/runtime-core/src/web.rs +83 -0
  31. package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
  32. package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
  33. package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
  34. package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
  35. package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
  36. package/dist/go-stage.mjs +193 -0
  37. package/dist/index.d.ts +221 -0
  38. package/dist/index.js +4393 -0
  39. package/dist/java-stage.mjs +111 -0
  40. package/dist/python-stage.mjs +90 -0
  41. package/dist/rustc-stage.mjs +301 -0
  42. package/dist/server-build-stage.mjs +2564 -0
  43. package/dist/server-runner-stage.mjs +155 -0
  44. package/licenses/fflate-MIT.txt +21 -0
  45. package/licenses/runtime-core-dependencies.html +6253 -0
  46. package/licenses/runtime-core-dependencies.json +3041 -0
  47. package/licenses/wasmer-sdk-MIT.txt +21 -0
  48. package/licenses/wasmer-sdk-dependencies.html +6901 -0
  49. package/licenses/wasmer-sdk-dependencies.json +3013 -0
  50. package/package.json +70 -0
  51. package/rust-toolchain.toml +5 -0
  52. package/testdata/wojjdg02-v2-text.hex +1 -0
  53. package/vendor/shared-buffer/Cargo.toml +22 -0
  54. package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
  55. package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
  56. package/vendor/shared-buffer/README.md +34 -0
  57. package/vendor/shared-buffer/src/lib.rs +58 -0
  58. package/vendor/shared-buffer/src/mmap.rs +250 -0
  59. package/vendor/shared-buffer/src/owned.rs +389 -0
  60. package/vendor/virtual-fs/Cargo.toml +181 -0
  61. package/vendor/virtual-fs/LICENSE +25 -0
  62. package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
  63. package/vendor/virtual-fs/src/arc_file.rs +182 -0
  64. package/vendor/virtual-fs/src/arc_fs.rs +68 -0
  65. package/vendor/virtual-fs/src/buffer_file.rs +103 -0
  66. package/vendor/virtual-fs/src/builder.rs +232 -0
  67. package/vendor/virtual-fs/src/combine_file.rs +101 -0
  68. package/vendor/virtual-fs/src/cow_file.rs +345 -0
  69. package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
  70. package/vendor/virtual-fs/src/empty_fs.rs +81 -0
  71. package/vendor/virtual-fs/src/filesystems.rs +108 -0
  72. package/vendor/virtual-fs/src/host_fs.rs +1390 -0
  73. package/vendor/virtual-fs/src/lib.rs +782 -0
  74. package/vendor/virtual-fs/src/limiter.rs +252 -0
  75. package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
  76. package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
  77. package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
  78. package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
  79. package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
  80. package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
  81. package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
  82. package/vendor/virtual-fs/src/null_file.rs +87 -0
  83. package/vendor/virtual-fs/src/ops.rs +364 -0
  84. package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
  85. package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
  86. package/vendor/virtual-fs/src/pipe.rs +603 -0
  87. package/vendor/virtual-fs/src/random_file.rs +88 -0
  88. package/vendor/virtual-fs/src/special_file.rs +108 -0
  89. package/vendor/virtual-fs/src/static_file.rs +133 -0
  90. package/vendor/virtual-fs/src/static_fs.rs +460 -0
  91. package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
  92. package/vendor/virtual-fs/src/trace_fs.rs +258 -0
  93. package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
  94. package/vendor/virtual-fs/src/zero_file.rs +90 -0
@@ -0,0 +1,1019 @@
1
+ use crate::capabilities::attach_capability_denials;
2
+ use crate::deterministic::{VirtualClock, attach_interactive_deterministic_imports};
3
+ use crate::filesystem::{
4
+ RuntimeProjectFilesystem, is_normalized_guest_path, runtime_project_files,
5
+ };
6
+ use crate::meter::{
7
+ CONTESTANT_METERING_MODULE, HOST_GAS_FUNCTION, INTERACTOR_METERING_MODULE,
8
+ instrument_wasm_with_host_meter,
9
+ };
10
+ use crate::module_imports::attach_declared_memory_imports;
11
+ use crate::module_policy::{
12
+ DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit,
13
+ rewrite_interactive_deterministic_imports,
14
+ };
15
+ use crate::output::{CappedOutput, OutputBudget, OutputCapture};
16
+ use crate::{
17
+ ExecutionTermination, InteractiveMetrics, InteractiveProcessResult, InteractiveProgram,
18
+ InteractiveRequest, InteractiveResult, RunError,
19
+ };
20
+ use std::collections::{BTreeMap, HashMap};
21
+ use std::io;
22
+ use std::pin::Pin;
23
+ use std::sync::{Arc, Mutex};
24
+ use std::task::{Context, Poll};
25
+ use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
26
+ use virtual_fs::{FsError, Pipe, PipeRx, PipeTx, VirtualFile};
27
+ use wasmer::{
28
+ AsStoreMut, Engine, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, RuntimeError,
29
+ };
30
+ use wasmer_types::StoreId;
31
+ use wasmer_wasix::bin_factory::spawn_exec_wasm;
32
+ use wasmer_wasix::runtime::module_cache::{self, HashedModuleData, ModuleCache};
33
+ use wasmer_wasix::{
34
+ PluggableRuntime, Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiVersion,
35
+ generate_import_object_from_env,
36
+ };
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
+ #[derive(Debug)]
97
+ struct InteractiveInput {
98
+ pipe: PipeRx,
99
+ }
100
+
101
+ impl AsyncRead for InteractiveInput {
102
+ fn poll_read(
103
+ mut self: Pin<&mut Self>,
104
+ context: &mut Context<'_>,
105
+ buffer: &mut ReadBuf<'_>,
106
+ ) -> Poll<io::Result<()>> {
107
+ Pin::new(&mut self.pipe).poll_read(context, buffer)
108
+ }
109
+ }
110
+
111
+ impl AsyncWrite for InteractiveInput {
112
+ fn poll_write(
113
+ self: Pin<&mut Self>,
114
+ _context: &mut Context<'_>,
115
+ _buffer: &[u8],
116
+ ) -> Poll<io::Result<usize>> {
117
+ Poll::Ready(Err(io::ErrorKind::Unsupported.into()))
118
+ }
119
+
120
+ fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
121
+ Poll::Ready(Ok(()))
122
+ }
123
+
124
+ fn poll_shutdown(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
125
+ Poll::Ready(Ok(()))
126
+ }
127
+ }
128
+
129
+ impl AsyncSeek for InteractiveInput {
130
+ fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
131
+ Ok(())
132
+ }
133
+
134
+ fn poll_complete(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<u64>> {
135
+ Poll::Ready(Ok(0))
136
+ }
137
+ }
138
+
139
+ impl VirtualFile for InteractiveInput {
140
+ fn last_accessed(&self) -> u64 {
141
+ 0
142
+ }
143
+ fn last_modified(&self) -> u64 {
144
+ 0
145
+ }
146
+ fn created_time(&self) -> u64 {
147
+ 0
148
+ }
149
+ fn size(&self) -> u64 {
150
+ 0
151
+ }
152
+ fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
153
+ Ok(())
154
+ }
155
+ fn unlink(&mut self) -> Result<(), FsError> {
156
+ Ok(())
157
+ }
158
+ fn get_special_fd(&self) -> Option<u32> {
159
+ Some(0)
160
+ }
161
+
162
+ fn poll_read_ready(
163
+ mut self: Pin<&mut Self>,
164
+ context: &mut Context<'_>,
165
+ ) -> Poll<io::Result<usize>> {
166
+ Pin::new(&mut self.pipe).poll_read_ready(context)
167
+ }
168
+
169
+ fn poll_write_ready(
170
+ self: Pin<&mut Self>,
171
+ _context: &mut Context<'_>,
172
+ ) -> Poll<io::Result<usize>> {
173
+ Poll::Ready(Err(io::ErrorKind::Unsupported.into()))
174
+ }
175
+ }
176
+
177
+ #[derive(Debug)]
178
+ struct InteractiveOutput {
179
+ pipe: PipeTx,
180
+ capture: CappedOutput,
181
+ }
182
+
183
+ impl AsyncWrite for InteractiveOutput {
184
+ fn poll_write(
185
+ mut self: Pin<&mut Self>,
186
+ context: &mut Context<'_>,
187
+ buffer: &[u8],
188
+ ) -> Poll<io::Result<usize>> {
189
+ match Pin::new(&mut self.capture).poll_write(context, buffer) {
190
+ Poll::Ready(Ok(written)) => {
191
+ Pin::new(&mut self.pipe).poll_write(context, &buffer[..written])
192
+ }
193
+ result => result,
194
+ }
195
+ }
196
+
197
+ fn poll_flush(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
198
+ Pin::new(&mut self.pipe).poll_flush(context)
199
+ }
200
+
201
+ fn poll_shutdown(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
202
+ Pin::new(&mut self.pipe).poll_shutdown(context)
203
+ }
204
+ }
205
+
206
+ impl AsyncRead for InteractiveOutput {
207
+ fn poll_read(
208
+ self: Pin<&mut Self>,
209
+ _context: &mut Context<'_>,
210
+ _buffer: &mut ReadBuf<'_>,
211
+ ) -> Poll<io::Result<()>> {
212
+ Poll::Ready(Ok(()))
213
+ }
214
+ }
215
+
216
+ impl AsyncSeek for InteractiveOutput {
217
+ fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
218
+ Ok(())
219
+ }
220
+ fn poll_complete(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<u64>> {
221
+ Poll::Ready(Ok(0))
222
+ }
223
+ }
224
+
225
+ impl VirtualFile for InteractiveOutput {
226
+ fn last_accessed(&self) -> u64 {
227
+ 0
228
+ }
229
+ fn last_modified(&self) -> u64 {
230
+ 0
231
+ }
232
+ fn created_time(&self) -> u64 {
233
+ 0
234
+ }
235
+ fn size(&self) -> u64 {
236
+ 0
237
+ }
238
+ fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
239
+ Ok(())
240
+ }
241
+ fn unlink(&mut self) -> Result<(), FsError> {
242
+ Ok(())
243
+ }
244
+ fn get_special_fd(&self) -> Option<u32> {
245
+ Some(1)
246
+ }
247
+
248
+ fn poll_read_ready(
249
+ self: Pin<&mut Self>,
250
+ _context: &mut Context<'_>,
251
+ ) -> Poll<io::Result<usize>> {
252
+ Poll::Ready(Ok(0))
253
+ }
254
+
255
+ fn poll_write_ready(
256
+ mut self: Pin<&mut Self>,
257
+ context: &mut Context<'_>,
258
+ ) -> Poll<io::Result<usize>> {
259
+ Pin::new(&mut self.capture).poll_write_ready(context)
260
+ }
261
+ }
262
+
263
+ struct PreparedProgram {
264
+ wasm: Vec<u8>,
265
+ operations: BTreeMap<String, u64>,
266
+ gas: Arc<GasBudget>,
267
+ protocol: OutputCapture,
268
+ stderr: OutputCapture,
269
+ filesystem: RuntimeProjectFilesystem,
270
+ clock: VirtualClock,
271
+ }
272
+
273
+ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult, RunError> {
274
+ 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
+ )?;
285
+ let maximum_memory = request
286
+ .contestant
287
+ .resources
288
+ .memory_limit_bytes
289
+ .max(request.interactor.resources.memory_limit_bytes);
290
+ let engine = interactive_engine(maximum_memory)?;
291
+ let module_cache: Arc<dyn ModuleCache + Send + Sync> = Arc::new(module_cache::in_memory());
292
+ let contestant_wasi = Arc::new(Mutex::new(None));
293
+ let contestant_runtime = interactive_runtime(
294
+ engine.clone(),
295
+ &request.determinism,
296
+ contestant.gas.clone(),
297
+ contestant.clock.clone(),
298
+ request.contestant.startup_entropy_bytes,
299
+ module_cache.clone(),
300
+ contestant_wasi.clone(),
301
+ )?;
302
+ let interactor_wasi = Arc::new(Mutex::new(None));
303
+ let interactor_runtime = interactive_runtime(
304
+ engine,
305
+ &request.determinism,
306
+ interactor.gas.clone(),
307
+ interactor.clock.clone(),
308
+ request.interactor.startup_entropy_bytes,
309
+ module_cache,
310
+ interactor_wasi.clone(),
311
+ )?;
312
+ let (contestant_end, interactor_end) = Pipe::channel();
313
+ let (contestant_output, contestant_input) = contestant_end.split();
314
+ let (interactor_output, interactor_input) = interactor_end.split();
315
+ let contestant_env = build_environment(
316
+ "contestant",
317
+ &request.contestant,
318
+ contestant_input,
319
+ contestant_output,
320
+ &contestant,
321
+ contestant_runtime.clone(),
322
+ )?;
323
+ *contestant_wasi
324
+ .lock()
325
+ .map_err(|error| RunError::Runtime(error.to_string()))? = Some(contestant_env.clone());
326
+ let interactor_env = build_environment(
327
+ "interactor",
328
+ &request.interactor,
329
+ interactor_input,
330
+ interactor_output,
331
+ &interactor,
332
+ interactor_runtime.clone(),
333
+ )?;
334
+ *interactor_wasi
335
+ .lock()
336
+ .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()),
339
+ "contestant",
340
+ contestant_env,
341
+ &contestant_runtime,
342
+ )
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()),
347
+ "interactor",
348
+ interactor_env,
349
+ &interactor_runtime,
350
+ )
351
+ .await
352
+ .map_err(|error| RunError::Compile(format!("failed to start interactor: {error}")))?;
353
+ let (contestant_status, interactor_status) = futures::join!(
354
+ contestant_handle.wait_finished(),
355
+ interactor_handle.wait_finished(),
356
+ );
357
+
358
+ let contestant_to_interactor = contestant.protocol.bytes();
359
+ let interactor_to_contestant = interactor.protocol.bytes();
360
+ let contestant_result = process_result(
361
+ contestant_status,
362
+ contestant,
363
+ contestant_to_interactor.len(),
364
+ )?;
365
+ let interactor_result = process_result(
366
+ interactor_status,
367
+ interactor,
368
+ interactor_to_contestant.len(),
369
+ )?;
370
+ Ok(InteractiveResult {
371
+ contestant: contestant_result,
372
+ interactor: interactor_result,
373
+ contestant_to_interactor,
374
+ interactor_to_contestant,
375
+ determinism: request.determinism,
376
+ })
377
+ }
378
+
379
+ fn prepare_program(
380
+ program: &InteractiveProgram,
381
+ metering_module: &'static str,
382
+ determinism: &crate::DeterminismConfig,
383
+ ) -> Result<PreparedProgram, RunError> {
384
+ let limited = enforce_memory_limit(&program.wasm, program.resources.memory_limit_bytes)
385
+ .map_err(RunError::Compile)?;
386
+ let metered =
387
+ instrument_wasm_with_host_meter(&limited, metering_module).map_err(RunError::Compile)?;
388
+ let executable = defer_start_section(&metered.wasm).map_err(RunError::Compile)?;
389
+ let wasm =
390
+ rewrite_interactive_deterministic_imports(&executable.wasm).map_err(RunError::Compile)?;
391
+ let limit = usize::try_from(program.resources.output_limit_bytes)
392
+ .map_err(|_| RunError::InvalidRequest("output limit exceeds host range".to_string()))?;
393
+ let output_budget = OutputBudget::new(limit);
394
+ let (protocol, _) = OutputCapture::new(output_budget.clone(), 1);
395
+ let (stderr, _) = OutputCapture::new(output_budget, 2);
396
+ let filesystem = runtime_project_files(&program.files, &[], determinism, &program.resources)?;
397
+ Ok(PreparedProgram {
398
+ wasm,
399
+ operations: metered.operations,
400
+ gas: Arc::new(GasBudget::new(program.resources.instruction_budget)),
401
+ protocol,
402
+ stderr,
403
+ filesystem,
404
+ clock: VirtualClock::new(determinism, program.resources.logical_time_limit_ms),
405
+ })
406
+ }
407
+
408
+ #[allow(clippy::too_many_arguments)]
409
+ fn build_environment(
410
+ name: &str,
411
+ program: &InteractiveProgram,
412
+ input: PipeRx,
413
+ output: PipeTx,
414
+ prepared: &PreparedProgram,
415
+ runtime: Arc<dyn Runtime + Send + Sync>,
416
+ ) -> Result<WasiEnv, RunError> {
417
+ let protocol_file = prepared.protocol.file(1);
418
+ let stderr_file = prepared.stderr.file(2);
419
+ let filesystem = prepared.filesystem.filesystem();
420
+ let mut builder = WasiEnv::builder(name)
421
+ .runtime(runtime)
422
+ .args(program.args.clone())
423
+ .envs(program.env.clone())
424
+ .stdin(Box::new(InteractiveInput { pipe: input }))
425
+ .stdout(Box::new(InteractiveOutput {
426
+ pipe: output,
427
+ capture: protocol_file,
428
+ }))
429
+ .stderr(Box::new(stderr_file))
430
+ .fs(filesystem);
431
+ builder
432
+ .add_preopen_build(|directory| directory.directory("/").read(true).write(true).create(true))
433
+ .map_err(|error| {
434
+ RunError::InvalidRequest(format!("failed to preopen interactive filesystem: {error}"))
435
+ })?;
436
+ if let Some(cwd) = &program.cwd {
437
+ builder.set_current_dir(cwd);
438
+ }
439
+ builder.build().map_err(|error| {
440
+ RunError::Compile(format!("failed to build {name} WASI environment: {error}"))
441
+ })
442
+ }
443
+
444
+ fn process_result(
445
+ status: Result<wasmer_wasix::wasmer_wasix_types::wasi::ExitCode, Arc<WasiRuntimeError>>,
446
+ prepared: PreparedProgram,
447
+ protocol_bytes: usize,
448
+ ) -> Result<InteractiveProcessResult, RunError> {
449
+ let stderr = prepared.stderr.bytes();
450
+ let output_exceeded = prepared.protocol.exceeded() || prepared.stderr.exceeded();
451
+ let (cost, exhausted) = prepared.gas.metrics()?;
452
+ let logical_time_exceeded = prepared.clock.limit_exceeded()?;
453
+ let (code, termination) = if prepared.filesystem.quota_exceeded() {
454
+ (137, ExecutionTermination::FilesystemLimit)
455
+ } else if output_exceeded {
456
+ (137, ExecutionTermination::OutputLimit)
457
+ } else if logical_time_exceeded {
458
+ (137, ExecutionTermination::LogicalTimeLimit)
459
+ } else if exhausted {
460
+ (137, ExecutionTermination::InstructionLimit)
461
+ } else {
462
+ match status {
463
+ Ok(code) => (code.raw(), ExecutionTermination::Exited),
464
+ Err(error) => match error.as_exit_code() {
465
+ Some(code) => (code.raw(), ExecutionTermination::Exited),
466
+ None => (1, ExecutionTermination::Trap),
467
+ },
468
+ }
469
+ };
470
+ let filesystem_metrics = prepared.filesystem.metrics();
471
+ Ok(InteractiveProcessResult {
472
+ code,
473
+ stderr,
474
+ termination,
475
+ metrics: InteractiveMetrics {
476
+ cost,
477
+ operations: prepared.operations,
478
+ logical_time_ns: prepared.clock.elapsed_ns()?,
479
+ filesystem_bytes: filesystem_metrics.bytes,
480
+ filesystem_entries: filesystem_metrics.entries,
481
+ protocol_bytes: protocol_bytes as u64,
482
+ stderr_bytes: prepared.stderr.bytes().len() as u64,
483
+ },
484
+ })
485
+ }
486
+
487
+ fn validate_request(request: &InteractiveRequest) -> Result<(), RunError> {
488
+ crate::run::validate_determinism(&request.determinism)?;
489
+ for (label, program) in [
490
+ ("contestant", &request.contestant),
491
+ ("interactor", &request.interactor),
492
+ ] {
493
+ if program.wasm.is_empty() {
494
+ return Err(RunError::InvalidRequest(format!(
495
+ "{label} Wasm must not be empty"
496
+ )));
497
+ }
498
+ crate::run::validate_resource_policy(&program.resources, label)?;
499
+ crate::run::validate_mounted_files(&program.files, label)?;
500
+ if program.startup_entropy_bytes > 4_096 {
501
+ return Err(RunError::InvalidRequest(format!(
502
+ "{label} startupEntropyBytes must be at most 4096"
503
+ )));
504
+ }
505
+ if let Some(cwd) = &program.cwd
506
+ && !is_normalized_guest_path(cwd)
507
+ {
508
+ return Err(RunError::InvalidRequest(format!(
509
+ "{label} cwd must be an absolute normalized guest path"
510
+ )));
511
+ }
512
+ }
513
+ Ok(())
514
+ }
515
+
516
+ fn interactive_runtime(
517
+ engine: Engine,
518
+ determinism: &crate::DeterminismConfig,
519
+ gas: Arc<GasBudget>,
520
+ clock: VirtualClock,
521
+ startup_entropy_bytes: u64,
522
+ module_cache: Arc<dyn ModuleCache + Send + Sync>,
523
+ environment: Arc<Mutex<Option<WasiEnv>>>,
524
+ ) -> Result<Arc<dyn Runtime + Send + Sync>, RunError> {
525
+ struct PendingInstance {
526
+ memory: Arc<Mutex<Option<Memory>>>,
527
+ wasi: WasiFunctionEnv,
528
+ }
529
+ let pending: Arc<Mutex<HashMap<StoreId, PendingInstance>>> =
530
+ Arc::new(Mutex::new(HashMap::new()));
531
+ let imports_pending = pending.clone();
532
+ let instance_pending = pending;
533
+ let config = determinism.clone();
534
+ let mut runtime = interactive_runtime_base(engine);
535
+ runtime.module_cache = module_cache;
536
+ runtime.with_additional_imports(move |module, store| {
537
+ let store_id = store.objects_mut().id();
538
+ let memory = Arc::new(Mutex::new(None));
539
+ let wasi_environment = environment
540
+ .lock()
541
+ .map_err(|error| io::Error::other(error.to_string()))?
542
+ .clone()
543
+ .ok_or_else(|| io::Error::other("interactive WASI environment is unavailable"))?;
544
+ let wasi = WasiFunctionEnv::new(&mut *store, wasi_environment);
545
+ if imports_pending
546
+ .lock()
547
+ .map_err(|error| io::Error::other(error.to_string()))?
548
+ .insert(
549
+ store_id,
550
+ PendingInstance {
551
+ memory: memory.clone(),
552
+ wasi: wasi.clone(),
553
+ },
554
+ )
555
+ .is_some()
556
+ {
557
+ return Err(
558
+ io::Error::other("interactive runtime received duplicate store state").into(),
559
+ );
560
+ }
561
+ let mut imports = Imports::new();
562
+ for version in [
563
+ WasiVersion::Snapshot1,
564
+ WasiVersion::Wasix32v1,
565
+ WasiVersion::Wasix64v1,
566
+ ] {
567
+ imports.extend(&generate_import_object_from_env(
568
+ &mut *store,
569
+ &wasi.env,
570
+ version,
571
+ ));
572
+ }
573
+ attach_interactive_deterministic_imports(
574
+ store,
575
+ &mut imports,
576
+ memory,
577
+ &config,
578
+ clock.clone(),
579
+ startup_entropy_bytes,
580
+ );
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
+ attach_capability_denials(store, module, &mut imports).map_err(io::Error::other)?;
598
+ attach_declared_memory_imports(store, module, &mut imports).map_err(io::Error::other)?;
599
+ Ok(imports)
600
+ });
601
+ runtime.with_instance_setup(move |_module, store, instance, imported_memory| {
602
+ let store_id = store.objects_mut().id();
603
+ let mut pending = instance_pending
604
+ .lock()
605
+ .map_err(|error| io::Error::other(error.to_string()))?
606
+ .remove(&store_id)
607
+ .ok_or_else(|| io::Error::other("interactive instance has no host state"))?;
608
+ let memory = instance
609
+ .exports
610
+ .get_memory("memory")
611
+ .cloned()
612
+ .ok()
613
+ .or_else(|| imported_memory.cloned())
614
+ .ok_or_else(|| io::Error::other("interactive instance has no linear memory"))?;
615
+ *pending
616
+ .memory
617
+ .lock()
618
+ .map_err(|error| io::Error::other(error.to_string()))? = Some(memory);
619
+ pending
620
+ .wasi
621
+ .initialize(&mut *store, instance.clone())
622
+ .map_err(io::Error::other)?;
623
+ if let Ok(initializer) = instance.exports.get_function(DEFERRED_START_EXPORT) {
624
+ initializer
625
+ .call(&mut *store, &[])
626
+ .map_err(io::Error::other)?;
627
+ }
628
+ Ok(())
629
+ });
630
+ Ok(Arc::new(runtime))
631
+ }
632
+
633
+ #[cfg(target_arch = "wasm32")]
634
+ fn interactive_runtime_base(engine: Engine) -> PluggableRuntime {
635
+ let tasks: Arc<dyn wasmer_wasix::runtime::task_manager::VirtualTaskManager> =
636
+ Arc::new(crate::run::web_runtime::WebTaskManager);
637
+ let mut runtime = PluggableRuntime::new(tasks);
638
+ runtime.set_engine(engine);
639
+ runtime
640
+ }
641
+
642
+ #[cfg(not(target_arch = "wasm32"))]
643
+ fn interactive_runtime_base(engine: Engine) -> PluggableRuntime {
644
+ use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager;
645
+ let tasks: Arc<dyn wasmer_wasix::runtime::task_manager::VirtualTaskManager> =
646
+ Arc::new(TokioTaskManager::default());
647
+ let mut runtime = PluggableRuntime::new(tasks);
648
+ runtime.set_engine(engine);
649
+ runtime
650
+ }
651
+
652
+ #[cfg(target_arch = "wasm32")]
653
+ fn interactive_engine(_memory_limit_bytes: u64) -> Result<Engine, RunError> {
654
+ Ok(Engine::default())
655
+ }
656
+
657
+ #[cfg(not(target_arch = "wasm32"))]
658
+ fn interactive_engine(memory_limit_bytes: u64) -> Result<Engine, RunError> {
659
+ use crate::memory::LimitingTunables;
660
+ use wasmer::Pages;
661
+ use wasmer::sys::{BaseTunables, Cranelift, NativeEngineExt, Target};
662
+ let pages = u32::try_from(memory_limit_bytes / 65_536).map_err(|_| {
663
+ RunError::InvalidRequest("memory limit exceeds Wasmer page range".to_string())
664
+ })?;
665
+ let base = BaseTunables::for_target(&Target::default());
666
+ let mut engine: Engine = Cranelift::default().into();
667
+ engine.set_tunables(LimitingTunables::new(base, Pages(pages)));
668
+ Ok(engine)
669
+ }
670
+
671
+ #[cfg(all(test, not(target_arch = "wasm32")))]
672
+ mod tests {
673
+ use super::interact;
674
+ use crate::{
675
+ DeterminismConfig, ExecutionTermination, InteractiveProgram, InteractiveRequest,
676
+ ResourcePolicy,
677
+ };
678
+ use std::collections::BTreeMap;
679
+
680
+ #[test]
681
+ fn connects_contestant_and_interactor_with_independent_metering() {
682
+ let contestant = wat::parse_str(
683
+ r#"(module
684
+ (import "wasi_snapshot_preview1" "fd_read"
685
+ (func $fd_read (param i32 i32 i32 i32) (result i32)))
686
+ (import "wasi_snapshot_preview1" "fd_write"
687
+ (func $fd_write (param i32 i32 i32 i32) (result i32)))
688
+ (memory (export "memory") 1)
689
+ (data (i32.const 80) "42\n")
690
+ (func (export "_start")
691
+ (i32.store (i32.const 0) (i32.const 64))
692
+ (i32.store (i32.const 4) (i32.const 3))
693
+ (drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))
694
+ (i32.store (i32.const 16) (i32.const 80))
695
+ (i32.store (i32.const 20) (i32.const 3))
696
+ (drop (call $fd_write (i32.const 1) (i32.const 16) (i32.const 1) (i32.const 24)))))"#,
697
+ )
698
+ .unwrap();
699
+ let interactor = wat::parse_str(
700
+ r#"(module
701
+ (import "wasi_snapshot_preview1" "fd_read"
702
+ (func $fd_read (param i32 i32 i32 i32) (result i32)))
703
+ (import "wasi_snapshot_preview1" "fd_write"
704
+ (func $fd_write (param i32 i32 i32 i32) (result i32)))
705
+ (memory (export "memory") 1)
706
+ (data (i32.const 80) "41\n")
707
+ (func (export "_start")
708
+ (i32.store (i32.const 16) (i32.const 80))
709
+ (i32.store (i32.const 20) (i32.const 3))
710
+ (drop (call $fd_write (i32.const 1) (i32.const 16) (i32.const 1) (i32.const 24)))
711
+ (i32.store (i32.const 0) (i32.const 64))
712
+ (i32.store (i32.const 4) (i32.const 3))
713
+ (drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))))"#,
714
+ )
715
+ .unwrap();
716
+ let runtime = tokio::runtime::Builder::new_current_thread()
717
+ .enable_all()
718
+ .build()
719
+ .unwrap();
720
+ let result = runtime
721
+ .block_on(interact(InteractiveRequest {
722
+ contestant: program(contestant),
723
+ interactor: program(interactor),
724
+ determinism: DeterminismConfig {
725
+ random_seed: 7,
726
+ realtime_epoch_ms: 946_684_800_000,
727
+ clock_step_ns: 1_000_000,
728
+ },
729
+ }))
730
+ .unwrap();
731
+ assert_eq!(result.contestant_to_interactor, b"42\n");
732
+ assert_eq!(result.interactor_to_contestant, b"41\n");
733
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
734
+ assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
735
+ assert_eq!(result.contestant.code, 0);
736
+ assert_eq!(result.interactor.code, 0);
737
+ assert!(result.contestant.metrics.cost > 0);
738
+ assert!(result.interactor.metrics.cost > 0);
739
+ }
740
+
741
+ #[test]
742
+ fn applies_filesystem_quota_independently_to_each_interactive_program() {
743
+ let writer = wat::parse_str(
744
+ r#"(module
745
+ (import "wasi_snapshot_preview1" "path_open"
746
+ (func $open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
747
+ (import "wasi_snapshot_preview1" "fd_write"
748
+ (func $write (param i32 i32 i32 i32) (result i32)))
749
+ (memory (export "memory") 1)
750
+ (data (i32.const 0) "created.txt")
751
+ (data (i32.const 80) "\60\00\00\00\08\00\00\00")
752
+ (data (i32.const 96) "12345678")
753
+ (func (export "_start")
754
+ i32.const 4 i32.const 0 i32.const 0 i32.const 11 i32.const 1
755
+ i64.const 64 i64.const 0 i32.const 0 i32.const 64 call $open drop
756
+ i32.const 64 i32.load i32.const 80 i32.const 1 i32.const 88 call $write drop))"#,
757
+ )
758
+ .unwrap();
759
+ let idle =
760
+ wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
761
+ .unwrap();
762
+ let mut contestant = program(writer);
763
+ contestant.resources.filesystem_write_limit_bytes = 4;
764
+ contestant.resources.filesystem_entry_limit = 1;
765
+ let runtime = tokio::runtime::Builder::new_current_thread()
766
+ .enable_all()
767
+ .build()
768
+ .unwrap();
769
+ let result = runtime
770
+ .block_on(interact(InteractiveRequest {
771
+ contestant,
772
+ interactor: program(idle),
773
+ determinism: DeterminismConfig {
774
+ random_seed: 7,
775
+ realtime_epoch_ms: 946_684_800_000,
776
+ clock_step_ns: 1_000_000,
777
+ },
778
+ }))
779
+ .unwrap();
780
+
781
+ assert_eq!(
782
+ result.contestant.termination,
783
+ ExecutionTermination::FilesystemLimit
784
+ );
785
+ assert_eq!(result.contestant.code, 137);
786
+ assert_eq!(result.contestant.metrics.filesystem_bytes, 0);
787
+ assert_eq!(result.contestant.metrics.filesystem_entries, 1);
788
+ assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
789
+ assert_eq!(result.interactor.metrics.filesystem_bytes, 0);
790
+ assert_eq!(result.interactor.metrics.filesystem_entries, 0);
791
+ }
792
+
793
+ #[test]
794
+ fn applies_logical_time_budgets_independently_to_each_interactive_program() {
795
+ let sleeper = wat::parse_str(
796
+ r#"(module
797
+ (import "wasix_32v1" "thread_sleep" (func $sleep (param i64) (result i32)))
798
+ (memory (export "memory") 1)
799
+ (func (export "_start") i64.const 11000000 call $sleep drop))"#,
800
+ )
801
+ .unwrap();
802
+ let idle =
803
+ wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
804
+ .unwrap();
805
+ let mut contestant = program(sleeper);
806
+ contestant.resources.logical_time_limit_ms = 10;
807
+ let runtime = tokio::runtime::Builder::new_current_thread()
808
+ .enable_all()
809
+ .build()
810
+ .unwrap();
811
+ let result = runtime
812
+ .block_on(interact(InteractiveRequest {
813
+ contestant,
814
+ interactor: program(idle),
815
+ determinism: DeterminismConfig {
816
+ random_seed: 7,
817
+ realtime_epoch_ms: 946_684_800_000,
818
+ clock_step_ns: 1_000_000,
819
+ },
820
+ }))
821
+ .unwrap();
822
+
823
+ assert_eq!(
824
+ result.contestant.termination,
825
+ ExecutionTermination::LogicalTimeLimit
826
+ );
827
+ assert_eq!(result.contestant.metrics.logical_time_ns, 10_000_000);
828
+ assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
829
+ assert_eq!(result.interactor.metrics.logical_time_ns, 0);
830
+ }
831
+
832
+ #[test]
833
+ fn interactive_wasi_poll_fast_forwards_the_process_clock() {
834
+ let sleeper = wat::parse_str(
835
+ r#"(module
836
+ (import "wasi_snapshot_preview1" "poll_oneoff"
837
+ (func $poll (param i32 i32 i32 i32) (result i32)))
838
+ (import "wasi_snapshot_preview1" "clock_time_get"
839
+ (func $clock (param i32 i64 i32) (result i32)))
840
+ (memory (export "memory") 1)
841
+ (func (export "_start")
842
+ i32.const 0 i64.const 7 i64.store
843
+ i32.const 8 i32.const 0 i32.store8
844
+ i32.const 16 i32.const 1 i32.store
845
+ i32.const 24 i64.const 5000000000 i64.store
846
+ i32.const 32 i64.const 1 i64.store
847
+ i32.const 40 i32.const 0 i32.store16
848
+ i32.const 0 i32.const 64 i32.const 1 i32.const 120 call $poll drop
849
+ i32.const 1 i64.const 0 i32.const 128 call $clock drop))"#,
850
+ )
851
+ .unwrap();
852
+ let idle =
853
+ wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
854
+ .unwrap();
855
+ let runtime = tokio::runtime::Builder::new_current_thread()
856
+ .enable_all()
857
+ .build()
858
+ .unwrap();
859
+ let result = runtime
860
+ .block_on(interact(InteractiveRequest {
861
+ contestant: program(sleeper),
862
+ interactor: program(idle),
863
+ determinism: DeterminismConfig {
864
+ random_seed: 7,
865
+ realtime_epoch_ms: 946_684_800_000,
866
+ clock_step_ns: 1_000_000,
867
+ },
868
+ }))
869
+ .unwrap();
870
+
871
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
872
+ assert_eq!(result.contestant.metrics.logical_time_ns, 5_001_000_000);
873
+ assert_eq!(result.interactor.metrics.logical_time_ns, 0);
874
+ }
875
+
876
+ #[test]
877
+ fn interactive_native_start_observes_the_initialized_process_clock() {
878
+ let initialized = wat::parse_str(
879
+ r#"(module
880
+ (import "wasi_snapshot_preview1" "clock_time_get"
881
+ (func $clock (param i32 i64 i32) (result i32)))
882
+ (memory (export "memory") 1)
883
+ (func $initialize
884
+ i32.const 1 i64.const 0 i32.const 0 call $clock drop)
885
+ (start $initialize)
886
+ (func (export "_start")))"#,
887
+ )
888
+ .unwrap();
889
+ let idle =
890
+ wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
891
+ .unwrap();
892
+ let runtime = tokio::runtime::Builder::new_current_thread()
893
+ .enable_all()
894
+ .build()
895
+ .unwrap();
896
+ let result = runtime
897
+ .block_on(interact(InteractiveRequest {
898
+ contestant: program(initialized),
899
+ interactor: program(idle),
900
+ determinism: DeterminismConfig {
901
+ random_seed: 7,
902
+ realtime_epoch_ms: 946_684_800_000,
903
+ clock_step_ns: 1_000_000,
904
+ },
905
+ }))
906
+ .unwrap();
907
+
908
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
909
+ assert_eq!(result.contestant.metrics.logical_time_ns, 1_000_000);
910
+ assert_eq!(result.interactor.metrics.logical_time_ns, 0);
911
+ }
912
+
913
+ #[test]
914
+ fn interactive_absolute_realtime_poll_uses_the_process_clock() {
915
+ let sleeper = wat::parse_str(
916
+ r#"(module
917
+ (import "wasi_snapshot_preview1" "poll_oneoff"
918
+ (func $poll (param i32 i32 i32 i32) (result i32)))
919
+ (import "wasi_snapshot_preview1" "clock_time_get"
920
+ (func $clock (param i32 i64 i32) (result i32)))
921
+ (memory (export "memory") 1)
922
+ (func (export "_start")
923
+ i32.const 0 i64.const 7 i64.store
924
+ i32.const 8 i32.const 0 i32.store8
925
+ i32.const 16 i32.const 0 i32.store
926
+ i32.const 24 i64.const 946684805000000000 i64.store
927
+ i32.const 32 i64.const 1 i64.store
928
+ i32.const 40 i32.const 1 i32.store16
929
+ i32.const 0 i32.const 64 i32.const 1 i32.const 120 call $poll drop
930
+ i32.const 0 i64.const 0 i32.const 128 call $clock drop))"#,
931
+ )
932
+ .unwrap();
933
+ let idle =
934
+ wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
935
+ .unwrap();
936
+ let runtime = tokio::runtime::Builder::new_current_thread()
937
+ .enable_all()
938
+ .build()
939
+ .unwrap();
940
+ let result = runtime
941
+ .block_on(interact(InteractiveRequest {
942
+ contestant: program(sleeper),
943
+ interactor: program(idle),
944
+ determinism: DeterminismConfig {
945
+ random_seed: 7,
946
+ realtime_epoch_ms: 946_684_800_000,
947
+ clock_step_ns: 1_000_000,
948
+ },
949
+ }))
950
+ .unwrap();
951
+
952
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
953
+ assert_eq!(result.contestant.metrics.logical_time_ns, 5_001_000_000);
954
+ assert_eq!(result.interactor.metrics.logical_time_ns, 0);
955
+ }
956
+
957
+ #[test]
958
+ fn interactive_ready_fd_wins_without_advancing_the_process_clock() {
959
+ let waiter = wat::parse_str(
960
+ r#"(module
961
+ (import "wasi_snapshot_preview1" "poll_oneoff"
962
+ (func $poll (param i32 i32 i32 i32) (result i32)))
963
+ (memory (export "memory") 1)
964
+ (func (export "_start")
965
+ i32.const 0 i64.const 1 i64.store
966
+ i32.const 8 i32.const 2 i32.store8
967
+ i32.const 16 i32.const 1 i32.store
968
+ i32.const 48 i64.const 2 i64.store
969
+ i32.const 56 i32.const 0 i32.store8
970
+ i32.const 64 i32.const 1 i32.store
971
+ i32.const 72 i64.const 5000000000 i64.store
972
+ i32.const 80 i64.const 1 i64.store
973
+ i32.const 88 i32.const 0 i32.store16
974
+ i32.const 0 i32.const 128 i32.const 2 i32.const 240 call $poll drop))"#,
975
+ )
976
+ .unwrap();
977
+ let idle =
978
+ wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
979
+ .unwrap();
980
+ let runtime = tokio::runtime::Builder::new_current_thread()
981
+ .enable_all()
982
+ .build()
983
+ .unwrap();
984
+ let result = runtime
985
+ .block_on(interact(InteractiveRequest {
986
+ contestant: program(waiter),
987
+ interactor: program(idle),
988
+ determinism: DeterminismConfig {
989
+ random_seed: 7,
990
+ realtime_epoch_ms: 946_684_800_000,
991
+ clock_step_ns: 1_000_000,
992
+ },
993
+ }))
994
+ .unwrap();
995
+
996
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
997
+ assert_eq!(result.contestant.metrics.logical_time_ns, 0);
998
+ assert_eq!(result.interactor.metrics.logical_time_ns, 0);
999
+ }
1000
+
1001
+ fn program(wasm: Vec<u8>) -> InteractiveProgram {
1002
+ InteractiveProgram {
1003
+ wasm,
1004
+ args: Vec::new(),
1005
+ env: BTreeMap::new(),
1006
+ files: BTreeMap::new(),
1007
+ cwd: Some("/".to_string()),
1008
+ startup_entropy_bytes: 0,
1009
+ resources: ResourcePolicy {
1010
+ instruction_budget: 1_000_000,
1011
+ logical_time_limit_ms: 60_000,
1012
+ memory_limit_bytes: 64 * 1024 * 1024,
1013
+ output_limit_bytes: 1024,
1014
+ filesystem_write_limit_bytes: 64 * 1024 * 1024,
1015
+ filesystem_entry_limit: 4_096,
1016
+ },
1017
+ }
1018
+ }
1019
+ }