@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.
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/THIRD_PARTY_NOTICES.md +302 -0
- package/crates/runtime-core/Cargo.lock +5099 -0
- package/crates/runtime-core/Cargo.toml +66 -0
- package/crates/runtime-core/README.md +47 -0
- package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
- package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
- package/crates/runtime-core/src/capabilities.rs +118 -0
- package/crates/runtime-core/src/compiler.rs +658 -0
- package/crates/runtime-core/src/contract.rs +5 -0
- package/crates/runtime-core/src/deterministic.rs +1051 -0
- package/crates/runtime-core/src/error.rs +58 -0
- package/crates/runtime-core/src/filesystem.rs +547 -0
- package/crates/runtime-core/src/filesystem_quota.rs +167 -0
- package/crates/runtime-core/src/go_compiler_session.rs +297 -0
- package/crates/runtime-core/src/interactive.rs +1019 -0
- package/crates/runtime-core/src/judge_package.rs +1539 -0
- package/crates/runtime-core/src/lib.rs +98 -0
- package/crates/runtime-core/src/memory.rs +84 -0
- package/crates/runtime-core/src/meter.rs +549 -0
- package/crates/runtime-core/src/module_imports.rs +149 -0
- package/crates/runtime-core/src/module_policy.rs +714 -0
- package/crates/runtime-core/src/output.rs +204 -0
- package/crates/runtime-core/src/run/mod.rs +208 -0
- package/crates/runtime-core/src/run/native.rs +260 -0
- package/crates/runtime-core/src/run/web.rs +229 -0
- package/crates/runtime-core/src/run/web_runtime.rs +109 -0
- package/crates/runtime-core/src/types.rs +268 -0
- package/crates/runtime-core/src/web.rs +83 -0
- package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
- package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
- package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
- package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
- package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
- package/dist/go-stage.mjs +193 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +4393 -0
- package/dist/java-stage.mjs +111 -0
- package/dist/python-stage.mjs +90 -0
- package/dist/rustc-stage.mjs +301 -0
- package/dist/server-build-stage.mjs +2564 -0
- package/dist/server-runner-stage.mjs +155 -0
- package/licenses/fflate-MIT.txt +21 -0
- package/licenses/runtime-core-dependencies.html +6253 -0
- package/licenses/runtime-core-dependencies.json +3041 -0
- package/licenses/wasmer-sdk-MIT.txt +21 -0
- package/licenses/wasmer-sdk-dependencies.html +6901 -0
- package/licenses/wasmer-sdk-dependencies.json +3013 -0
- package/package.json +70 -0
- package/rust-toolchain.toml +5 -0
- package/testdata/wojjdg02-v2-text.hex +1 -0
- package/vendor/shared-buffer/Cargo.toml +22 -0
- package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
- package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
- package/vendor/shared-buffer/README.md +34 -0
- package/vendor/shared-buffer/src/lib.rs +58 -0
- package/vendor/shared-buffer/src/mmap.rs +250 -0
- package/vendor/shared-buffer/src/owned.rs +389 -0
- package/vendor/virtual-fs/Cargo.toml +181 -0
- package/vendor/virtual-fs/LICENSE +25 -0
- package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
- package/vendor/virtual-fs/src/arc_file.rs +182 -0
- package/vendor/virtual-fs/src/arc_fs.rs +68 -0
- package/vendor/virtual-fs/src/buffer_file.rs +103 -0
- package/vendor/virtual-fs/src/builder.rs +232 -0
- package/vendor/virtual-fs/src/combine_file.rs +101 -0
- package/vendor/virtual-fs/src/cow_file.rs +345 -0
- package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
- package/vendor/virtual-fs/src/empty_fs.rs +81 -0
- package/vendor/virtual-fs/src/filesystems.rs +108 -0
- package/vendor/virtual-fs/src/host_fs.rs +1390 -0
- package/vendor/virtual-fs/src/lib.rs +782 -0
- package/vendor/virtual-fs/src/limiter.rs +252 -0
- package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
- package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
- package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
- package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
- package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
- package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
- package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
- package/vendor/virtual-fs/src/null_file.rs +87 -0
- package/vendor/virtual-fs/src/ops.rs +364 -0
- package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
- package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
- package/vendor/virtual-fs/src/pipe.rs +603 -0
- package/vendor/virtual-fs/src/random_file.rs +88 -0
- package/vendor/virtual-fs/src/special_file.rs +108 -0
- package/vendor/virtual-fs/src/static_file.rs +133 -0
- package/vendor/virtual-fs/src/static_fs.rs +460 -0
- package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
- package/vendor/virtual-fs/src/trace_fs.rs +258 -0
- package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
- 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
|
+
}
|