@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,204 @@
|
|
|
1
|
+
use std::io;
|
|
2
|
+
use std::pin::Pin;
|
|
3
|
+
use std::sync::{Arc, Mutex};
|
|
4
|
+
use std::task::{Context, Poll};
|
|
5
|
+
use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
|
|
6
|
+
use virtual_fs::{FsError, VirtualFile};
|
|
7
|
+
|
|
8
|
+
#[derive(Clone, Debug)]
|
|
9
|
+
pub struct OutputCapture {
|
|
10
|
+
state: Arc<Mutex<State>>,
|
|
11
|
+
budget: OutputBudget,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[derive(Debug)]
|
|
15
|
+
struct State {
|
|
16
|
+
bytes: Vec<u8>,
|
|
17
|
+
exceeded: bool,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Clone, Debug)]
|
|
21
|
+
pub struct OutputBudget {
|
|
22
|
+
state: Arc<Mutex<BudgetState>>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug)]
|
|
26
|
+
struct BudgetState {
|
|
27
|
+
used: usize,
|
|
28
|
+
limit: usize,
|
|
29
|
+
exceeded: bool,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl OutputCapture {
|
|
33
|
+
pub fn new(budget: OutputBudget, special_fd: u32) -> (Self, CappedOutput) {
|
|
34
|
+
let capture = Self {
|
|
35
|
+
state: Arc::new(Mutex::new(State {
|
|
36
|
+
bytes: Vec::new(),
|
|
37
|
+
exceeded: false,
|
|
38
|
+
})),
|
|
39
|
+
budget,
|
|
40
|
+
};
|
|
41
|
+
let file = CappedOutput {
|
|
42
|
+
state: capture.state.clone(),
|
|
43
|
+
budget: capture.budget.clone(),
|
|
44
|
+
special_fd: Some(special_fd),
|
|
45
|
+
};
|
|
46
|
+
(capture, file)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn bytes(&self) -> Vec<u8> {
|
|
50
|
+
self.state
|
|
51
|
+
.lock()
|
|
52
|
+
.expect("output capture lock poisoned")
|
|
53
|
+
.bytes
|
|
54
|
+
.clone()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn exceeded(&self) -> bool {
|
|
58
|
+
self.state
|
|
59
|
+
.lock()
|
|
60
|
+
.expect("output capture lock poisoned")
|
|
61
|
+
.exceeded
|
|
62
|
+
|| self.budget.exceeded()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn file(&self, special_fd: u32) -> CappedOutput {
|
|
66
|
+
CappedOutput {
|
|
67
|
+
state: self.state.clone(),
|
|
68
|
+
budget: self.budget.clone(),
|
|
69
|
+
special_fd: Some(special_fd),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl OutputBudget {
|
|
75
|
+
pub fn new(limit: usize) -> Self {
|
|
76
|
+
Self {
|
|
77
|
+
state: Arc::new(Mutex::new(BudgetState {
|
|
78
|
+
used: 0,
|
|
79
|
+
limit,
|
|
80
|
+
exceeded: false,
|
|
81
|
+
})),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn exceeded(&self) -> bool {
|
|
86
|
+
self.state
|
|
87
|
+
.lock()
|
|
88
|
+
.expect("output budget lock poisoned")
|
|
89
|
+
.exceeded
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Debug)]
|
|
94
|
+
pub struct CappedOutput {
|
|
95
|
+
state: Arc<Mutex<State>>,
|
|
96
|
+
budget: OutputBudget,
|
|
97
|
+
special_fd: Option<u32>,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl AsyncWrite for CappedOutput {
|
|
101
|
+
fn poll_write(
|
|
102
|
+
self: Pin<&mut Self>,
|
|
103
|
+
_cx: &mut Context<'_>,
|
|
104
|
+
input: &[u8],
|
|
105
|
+
) -> Poll<io::Result<usize>> {
|
|
106
|
+
let mut budget = self
|
|
107
|
+
.budget
|
|
108
|
+
.state
|
|
109
|
+
.lock()
|
|
110
|
+
.expect("output budget lock poisoned");
|
|
111
|
+
let remaining = budget.limit.saturating_sub(budget.used);
|
|
112
|
+
let mut state = self.state.lock().expect("output capture lock poisoned");
|
|
113
|
+
if input.len() > remaining {
|
|
114
|
+
state.bytes.extend_from_slice(&input[..remaining]);
|
|
115
|
+
state.exceeded = true;
|
|
116
|
+
budget.used = budget.limit;
|
|
117
|
+
budget.exceeded = true;
|
|
118
|
+
return Poll::Ready(Err(io::Error::new(
|
|
119
|
+
io::ErrorKind::FileTooLarge,
|
|
120
|
+
"output limit exceeded",
|
|
121
|
+
)));
|
|
122
|
+
}
|
|
123
|
+
state.bytes.extend_from_slice(input);
|
|
124
|
+
budget.used += input.len();
|
|
125
|
+
Poll::Ready(Ok(input.len()))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
129
|
+
Poll::Ready(Ok(()))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
133
|
+
Poll::Ready(Ok(()))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
impl AsyncRead for CappedOutput {
|
|
138
|
+
fn poll_read(
|
|
139
|
+
self: Pin<&mut Self>,
|
|
140
|
+
_cx: &mut Context<'_>,
|
|
141
|
+
_buffer: &mut ReadBuf<'_>,
|
|
142
|
+
) -> Poll<io::Result<()>> {
|
|
143
|
+
Poll::Ready(Ok(()))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
impl AsyncSeek for CappedOutput {
|
|
148
|
+
fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
|
|
149
|
+
Ok(())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
|
|
153
|
+
Poll::Ready(Ok(0))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
impl VirtualFile for CappedOutput {
|
|
158
|
+
fn last_accessed(&self) -> u64 {
|
|
159
|
+
0
|
|
160
|
+
}
|
|
161
|
+
fn last_modified(&self) -> u64 {
|
|
162
|
+
0
|
|
163
|
+
}
|
|
164
|
+
fn created_time(&self) -> u64 {
|
|
165
|
+
0
|
|
166
|
+
}
|
|
167
|
+
fn size(&self) -> u64 {
|
|
168
|
+
0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
|
|
172
|
+
// stdout/stderr are streams; WASI metadata updates must not allocate.
|
|
173
|
+
Ok(())
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn unlink(&mut self) -> Result<(), FsError> {
|
|
177
|
+
Ok(())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn get_special_fd(&self) -> Option<u32> {
|
|
181
|
+
self.special_fd
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
|
|
185
|
+
Poll::Ready(Ok(0))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn poll_write_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
|
|
189
|
+
let budget = self
|
|
190
|
+
.budget
|
|
191
|
+
.state
|
|
192
|
+
.lock()
|
|
193
|
+
.expect("output budget lock poisoned");
|
|
194
|
+
let remaining = budget.limit.saturating_sub(budget.used);
|
|
195
|
+
if remaining == 0 {
|
|
196
|
+
Poll::Ready(Err(io::Error::new(
|
|
197
|
+
io::ErrorKind::FileTooLarge,
|
|
198
|
+
"output limit exceeded",
|
|
199
|
+
)))
|
|
200
|
+
} else {
|
|
201
|
+
Poll::Ready(Ok(remaining.min(8_192)))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
use crate::{ResourcePolicy, RunError, RunRequest, RunResult};
|
|
2
|
+
use std::collections::BTreeMap;
|
|
3
|
+
|
|
4
|
+
pub(crate) const MAX_MOUNTED_FILES: usize = 32_768;
|
|
5
|
+
pub(crate) const MAX_MOUNTED_FILE_BYTES: usize = 256 * 1024 * 1024;
|
|
6
|
+
pub(crate) const MAX_MOUNTED_FILES_BYTES: usize = 512 * 1024 * 1024;
|
|
7
|
+
pub(crate) const MAX_FILESYSTEM_WRITE_BYTES: u64 = 512 * 1024 * 1024;
|
|
8
|
+
pub(crate) const MAX_FILESYSTEM_ENTRIES: u64 = 65_536;
|
|
9
|
+
pub(crate) const MAX_LOGICAL_TIME_LIMIT_MS: u64 = 9_007_199_254;
|
|
10
|
+
pub(crate) const MAX_REALTIME_EPOCH_MS: u64 = 18_446_744_073_000;
|
|
11
|
+
pub(crate) const MAX_CLOCK_STEP_NS: u64 = 1_000_000_000;
|
|
12
|
+
|
|
13
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
14
|
+
mod native;
|
|
15
|
+
#[cfg(target_arch = "wasm32")]
|
|
16
|
+
mod web;
|
|
17
|
+
#[cfg(target_arch = "wasm32")]
|
|
18
|
+
pub(crate) mod web_runtime;
|
|
19
|
+
|
|
20
|
+
pub fn run(request: RunRequest) -> Result<RunResult, RunError> {
|
|
21
|
+
validate(&request)?;
|
|
22
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
23
|
+
return native::run(request);
|
|
24
|
+
#[cfg(target_arch = "wasm32")]
|
|
25
|
+
return web::run(request);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn validate(request: &RunRequest) -> Result<(), RunError> {
|
|
29
|
+
validate_resource_policy(&request.resources, "")?;
|
|
30
|
+
validate_mounted_files(&request.files, "")?;
|
|
31
|
+
validate_determinism(&request.determinism)?;
|
|
32
|
+
if request.startup_entropy_bytes > 4_096 {
|
|
33
|
+
return Err(RunError::InvalidRequest(
|
|
34
|
+
"startupEntropyBytes must be at most 4096".to_string(),
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
if let Some(cwd) = &request.cwd
|
|
38
|
+
&& !crate::filesystem::is_normalized_guest_path(cwd)
|
|
39
|
+
{
|
|
40
|
+
return Err(RunError::InvalidRequest(
|
|
41
|
+
"cwd must be an absolute normalized guest path".to_string(),
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
if request.output_paths.len() > 256 {
|
|
45
|
+
return Err(RunError::InvalidRequest(
|
|
46
|
+
"outputPaths may contain at most 256 entries".to_string(),
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
let mut preceding: Option<&str> = None;
|
|
50
|
+
for path in &request.output_paths {
|
|
51
|
+
if !crate::filesystem::is_normalized_guest_path(path) || path == "/" {
|
|
52
|
+
return Err(RunError::InvalidRequest(format!(
|
|
53
|
+
"output path must be an absolute normalized file path: {path}"
|
|
54
|
+
)));
|
|
55
|
+
}
|
|
56
|
+
if preceding.is_some_and(|value| value >= path.as_str()) {
|
|
57
|
+
return Err(RunError::InvalidRequest(
|
|
58
|
+
"outputPaths must be sorted and unique".to_string(),
|
|
59
|
+
));
|
|
60
|
+
}
|
|
61
|
+
preceding = Some(path);
|
|
62
|
+
}
|
|
63
|
+
Ok(())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub(crate) fn validate_determinism(determinism: &crate::DeterminismConfig) -> Result<(), RunError> {
|
|
67
|
+
if determinism.random_seed > u32::MAX as u64 {
|
|
68
|
+
return Err(RunError::InvalidRequest(
|
|
69
|
+
"randomSeed must fit an unsigned 32-bit integer".to_string(),
|
|
70
|
+
));
|
|
71
|
+
}
|
|
72
|
+
if determinism.realtime_epoch_ms > MAX_REALTIME_EPOCH_MS {
|
|
73
|
+
return Err(RunError::InvalidRequest(format!(
|
|
74
|
+
"realtimeEpochMs must be in 0..={MAX_REALTIME_EPOCH_MS}"
|
|
75
|
+
)));
|
|
76
|
+
}
|
|
77
|
+
if determinism.clock_step_ns == 0 || determinism.clock_step_ns > MAX_CLOCK_STEP_NS {
|
|
78
|
+
return Err(RunError::InvalidRequest(format!(
|
|
79
|
+
"clockStepNs must be in 1..={MAX_CLOCK_STEP_NS}"
|
|
80
|
+
)));
|
|
81
|
+
}
|
|
82
|
+
Ok(())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub(crate) fn validate_resource_policy(
|
|
86
|
+
resources: &ResourcePolicy,
|
|
87
|
+
label: &str,
|
|
88
|
+
) -> Result<(), RunError> {
|
|
89
|
+
let prefix = if label.is_empty() {
|
|
90
|
+
String::new()
|
|
91
|
+
} else {
|
|
92
|
+
format!("{label} ")
|
|
93
|
+
};
|
|
94
|
+
if resources.instruction_budget == 0 || resources.instruction_budget > i64::MAX as u64 {
|
|
95
|
+
return Err(RunError::InvalidRequest(format!(
|
|
96
|
+
"{prefix}instructionBudget must be in 1..=i64::MAX"
|
|
97
|
+
)));
|
|
98
|
+
}
|
|
99
|
+
if resources.logical_time_limit_ms == 0
|
|
100
|
+
|| resources.logical_time_limit_ms > MAX_LOGICAL_TIME_LIMIT_MS
|
|
101
|
+
{
|
|
102
|
+
return Err(RunError::InvalidRequest(format!(
|
|
103
|
+
"{prefix}logicalTimeLimitMs must be in 1..={MAX_LOGICAL_TIME_LIMIT_MS}"
|
|
104
|
+
)));
|
|
105
|
+
}
|
|
106
|
+
if resources.memory_limit_bytes == 0 || !resources.memory_limit_bytes.is_multiple_of(65_536) {
|
|
107
|
+
return Err(RunError::InvalidRequest(format!(
|
|
108
|
+
"{prefix}memoryLimitBytes must be a positive multiple of 64 KiB"
|
|
109
|
+
)));
|
|
110
|
+
}
|
|
111
|
+
if resources.output_limit_bytes == 0 || usize::try_from(resources.output_limit_bytes).is_err() {
|
|
112
|
+
return Err(RunError::InvalidRequest(format!(
|
|
113
|
+
"{prefix}outputLimitBytes is not representable on this host"
|
|
114
|
+
)));
|
|
115
|
+
}
|
|
116
|
+
if resources.filesystem_write_limit_bytes == 0
|
|
117
|
+
|| resources.filesystem_write_limit_bytes > MAX_FILESYSTEM_WRITE_BYTES
|
|
118
|
+
|| usize::try_from(resources.filesystem_write_limit_bytes).is_err()
|
|
119
|
+
{
|
|
120
|
+
return Err(RunError::InvalidRequest(format!(
|
|
121
|
+
"{prefix}filesystemWriteLimitBytes must be in 1..={MAX_FILESYSTEM_WRITE_BYTES}"
|
|
122
|
+
)));
|
|
123
|
+
}
|
|
124
|
+
if resources.filesystem_entry_limit == 0
|
|
125
|
+
|| resources.filesystem_entry_limit > MAX_FILESYSTEM_ENTRIES
|
|
126
|
+
|| usize::try_from(resources.filesystem_entry_limit).is_err()
|
|
127
|
+
{
|
|
128
|
+
return Err(RunError::InvalidRequest(format!(
|
|
129
|
+
"{prefix}filesystemEntryLimit must be in 1..={MAX_FILESYSTEM_ENTRIES}"
|
|
130
|
+
)));
|
|
131
|
+
}
|
|
132
|
+
Ok(())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pub(crate) fn validate_mounted_files(
|
|
136
|
+
files: &BTreeMap<String, serde_bytes::ByteBuf>,
|
|
137
|
+
label: &str,
|
|
138
|
+
) -> Result<(), RunError> {
|
|
139
|
+
let prefix = if label.is_empty() {
|
|
140
|
+
String::new()
|
|
141
|
+
} else {
|
|
142
|
+
format!("{label} ")
|
|
143
|
+
};
|
|
144
|
+
if files.len() > MAX_MOUNTED_FILES {
|
|
145
|
+
return Err(RunError::InvalidRequest(format!(
|
|
146
|
+
"{prefix}mounted files exceed the {MAX_MOUNTED_FILES}-entry limit"
|
|
147
|
+
)));
|
|
148
|
+
}
|
|
149
|
+
let mut total = 0usize;
|
|
150
|
+
for (path, contents) in files {
|
|
151
|
+
if contents.len() > MAX_MOUNTED_FILE_BYTES {
|
|
152
|
+
return Err(RunError::InvalidRequest(format!(
|
|
153
|
+
"{prefix}mounted file '{path}' exceeds {MAX_MOUNTED_FILE_BYTES} bytes"
|
|
154
|
+
)));
|
|
155
|
+
}
|
|
156
|
+
total = total.checked_add(contents.len()).ok_or_else(|| {
|
|
157
|
+
RunError::InvalidRequest(format!("{prefix}mounted file size overflows host range"))
|
|
158
|
+
})?;
|
|
159
|
+
if total > MAX_MOUNTED_FILES_BYTES {
|
|
160
|
+
return Err(RunError::InvalidRequest(format!(
|
|
161
|
+
"{prefix}mounted files exceed {MAX_MOUNTED_FILES_BYTES} total bytes"
|
|
162
|
+
)));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
Ok(())
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn canonical_trap_message(message: &str) -> String {
|
|
169
|
+
let mut root = message.trim().lines().next().unwrap_or_default().trim();
|
|
170
|
+
loop {
|
|
171
|
+
let unwrapped = ["RuntimeError: ", "js: ", "user: "]
|
|
172
|
+
.iter()
|
|
173
|
+
.find_map(|prefix| root.strip_prefix(prefix));
|
|
174
|
+
let Some(unwrapped) = unwrapped else {
|
|
175
|
+
break;
|
|
176
|
+
};
|
|
177
|
+
root = unwrapped.trim_start();
|
|
178
|
+
}
|
|
179
|
+
if root.is_empty() {
|
|
180
|
+
"RuntimeError".to_string()
|
|
181
|
+
} else {
|
|
182
|
+
format!("RuntimeError: {root}")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[cfg(test)]
|
|
187
|
+
mod tests {
|
|
188
|
+
use super::canonical_trap_message;
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn removes_native_and_javascript_runtime_wrappers_from_traps() {
|
|
192
|
+
let root = "WASM-OJ denied nondeterministic capability wasix_32v1.thread_spawn";
|
|
193
|
+
assert_eq!(
|
|
194
|
+
canonical_trap_message(&format!("RuntimeError: {root}")),
|
|
195
|
+
format!("RuntimeError: {root}")
|
|
196
|
+
);
|
|
197
|
+
assert_eq!(
|
|
198
|
+
canonical_trap_message(&format!("RuntimeError: js: RuntimeError: user: {root}")),
|
|
199
|
+
format!("RuntimeError: {root}")
|
|
200
|
+
);
|
|
201
|
+
assert_eq!(
|
|
202
|
+
canonical_trap_message(&format!(
|
|
203
|
+
"RuntimeError: {root}\n at <unnamed> (<module>[4]:0x1a4)"
|
|
204
|
+
)),
|
|
205
|
+
format!("RuntimeError: {root}")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
use crate::capabilities::attach_capability_denials;
|
|
2
|
+
use crate::deterministic::{VirtualClock, attach_deterministic_imports};
|
|
3
|
+
use crate::filesystem::{read_files_bounded, runtime_project_files};
|
|
4
|
+
use crate::memory::LimitingTunables;
|
|
5
|
+
use crate::meter::{CostPoints, METER_MODEL, instrument_wasm, meter_state, remaining_points};
|
|
6
|
+
use crate::module_imports::attach_imported_memory;
|
|
7
|
+
use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
|
|
8
|
+
use crate::output::{OutputBudget, OutputCapture};
|
|
9
|
+
use crate::{ExecutionMetrics, ExecutionTermination, RunError, RunRequest, RunResult};
|
|
10
|
+
use std::io::Write;
|
|
11
|
+
use std::sync::{Arc, Mutex};
|
|
12
|
+
use wasmer::sys::{BaseTunables, Cranelift, NativeEngineExt, Target};
|
|
13
|
+
use wasmer::{Engine, Instance, Memory, Module, Pages, Store};
|
|
14
|
+
use wasmer_wasix::{
|
|
15
|
+
Pipe, WasiEnv, WasiError, WasiModuleInstanceHandles, WasiModuleTreeHandles, wasmer_wasix_types,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
pub fn run(request: RunRequest) -> Result<RunResult, RunError> {
|
|
19
|
+
let runtime = if tokio::runtime::Handle::try_current().is_err() {
|
|
20
|
+
Some(
|
|
21
|
+
tokio::runtime::Builder::new_current_thread()
|
|
22
|
+
.enable_all()
|
|
23
|
+
.build()
|
|
24
|
+
.map_err(|error| {
|
|
25
|
+
RunError::Runtime(format!("failed to initialize Tokio: {error}"))
|
|
26
|
+
})?,
|
|
27
|
+
)
|
|
28
|
+
} else {
|
|
29
|
+
None
|
|
30
|
+
};
|
|
31
|
+
let _runtime_guard = runtime.as_ref().map(tokio::runtime::Runtime::enter);
|
|
32
|
+
run_in_runtime(request)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn run_in_runtime(request: RunRequest) -> Result<RunResult, RunError> {
|
|
36
|
+
let limited = enforce_memory_limit(&request.wasm, request.resources.memory_limit_bytes)
|
|
37
|
+
.map_err(RunError::Compile)?;
|
|
38
|
+
let metered = instrument_wasm(&limited, request.resources.instruction_budget)
|
|
39
|
+
.map_err(RunError::Compile)?;
|
|
40
|
+
let executable = defer_start_section(&metered.wasm).map_err(RunError::Compile)?;
|
|
41
|
+
|
|
42
|
+
let pages = u32::try_from(request.resources.memory_limit_bytes / 65_536).map_err(|_| {
|
|
43
|
+
RunError::InvalidRequest("memory limit exceeds Wasmer page range".to_string())
|
|
44
|
+
})?;
|
|
45
|
+
let base = BaseTunables::for_target(&Target::default());
|
|
46
|
+
let mut engine: Engine = Cranelift::default().into();
|
|
47
|
+
engine.set_tunables(LimitingTunables::new(base, Pages(pages)));
|
|
48
|
+
let wasi_engine = engine.clone();
|
|
49
|
+
let mut store = Store::new(engine);
|
|
50
|
+
let module = Module::new(&store, &executable.wasm).map_err(|error| {
|
|
51
|
+
RunError::Compile(format!("failed to compile instrumented module: {error}"))
|
|
52
|
+
})?;
|
|
53
|
+
|
|
54
|
+
let (mut stdin_writer, stdin_reader) = Pipe::channel();
|
|
55
|
+
stdin_writer
|
|
56
|
+
.write_all(&request.stdin)
|
|
57
|
+
.map_err(|error| RunError::Io(error.to_string()))?;
|
|
58
|
+
drop(stdin_writer);
|
|
59
|
+
|
|
60
|
+
let output_limit = usize::try_from(request.resources.output_limit_bytes)
|
|
61
|
+
.map_err(|_| RunError::InvalidRequest("output limit exceeds host range".to_string()))?;
|
|
62
|
+
let output_budget = OutputBudget::new(output_limit);
|
|
63
|
+
let (stdout_capture, stdout_file) = OutputCapture::new(output_budget.clone(), 1);
|
|
64
|
+
let (stderr_capture, stderr_file) = OutputCapture::new(output_budget, 2);
|
|
65
|
+
|
|
66
|
+
let project_filesystem = runtime_project_files(
|
|
67
|
+
&request.files,
|
|
68
|
+
&request.output_paths,
|
|
69
|
+
&request.determinism,
|
|
70
|
+
&request.resources,
|
|
71
|
+
)?;
|
|
72
|
+
let filesystem = project_filesystem.filesystem();
|
|
73
|
+
let mut builder = WasiEnv::builder("app")
|
|
74
|
+
.engine(wasi_engine)
|
|
75
|
+
.args(request.args.clone())
|
|
76
|
+
.envs(request.env.clone())
|
|
77
|
+
.stdin(Box::new(stdin_reader))
|
|
78
|
+
.stdout(Box::new(stdout_file))
|
|
79
|
+
.stderr(Box::new(stderr_file))
|
|
80
|
+
.fs(filesystem.clone());
|
|
81
|
+
builder
|
|
82
|
+
.add_preopen_build(|directory| directory.directory("/").read(true).write(true).create(true))
|
|
83
|
+
.map_err(|error| {
|
|
84
|
+
RunError::InvalidRequest(format!("failed to preopen guest filesystem: {error}"))
|
|
85
|
+
})?;
|
|
86
|
+
if let Some(cwd) = &request.cwd {
|
|
87
|
+
builder.set_current_dir(cwd);
|
|
88
|
+
}
|
|
89
|
+
let mut sandbox = builder.finalize(&mut store).map_err(|error| {
|
|
90
|
+
RunError::Compile(format!("failed to finalize WASI environment: {error}"))
|
|
91
|
+
})?;
|
|
92
|
+
let mut imports = sandbox
|
|
93
|
+
.import_object_for_all_wasi_versions(&mut store, &module)
|
|
94
|
+
.map_err(|error| RunError::Compile(format!("failed to create WASI imports: {error}")))?;
|
|
95
|
+
let memory_slot: Arc<Mutex<Option<Memory>>> = Arc::new(Mutex::new(None));
|
|
96
|
+
let clock = VirtualClock::new(
|
|
97
|
+
&request.determinism,
|
|
98
|
+
request.resources.logical_time_limit_ms,
|
|
99
|
+
);
|
|
100
|
+
attach_deterministic_imports(
|
|
101
|
+
&mut store,
|
|
102
|
+
&mut imports,
|
|
103
|
+
memory_slot.clone(),
|
|
104
|
+
&request.determinism,
|
|
105
|
+
clock.clone(),
|
|
106
|
+
request.startup_entropy_bytes,
|
|
107
|
+
);
|
|
108
|
+
attach_capability_denials(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
|
|
109
|
+
let imported_memory =
|
|
110
|
+
attach_imported_memory(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
|
|
111
|
+
|
|
112
|
+
let instance = Instance::new(&mut store, &module, &imports)
|
|
113
|
+
.map_err(|error| RunError::Compile(format!("failed to instantiate module: {error}")))?;
|
|
114
|
+
let meter = meter_state(&mut store, &instance).map_err(RunError::Runtime)?;
|
|
115
|
+
let guest_memory = instance
|
|
116
|
+
.exports
|
|
117
|
+
.get_memory("memory")
|
|
118
|
+
.cloned()
|
|
119
|
+
.ok()
|
|
120
|
+
.or(imported_memory)
|
|
121
|
+
.ok_or_else(|| RunError::Compile("module has no guest linear memory".to_string()))?;
|
|
122
|
+
*memory_slot
|
|
123
|
+
.lock()
|
|
124
|
+
.map_err(|error| RunError::Runtime(error.to_string()))? = Some(guest_memory.clone());
|
|
125
|
+
let handles = WasiModuleTreeHandles::Static(WasiModuleInstanceHandles::new(
|
|
126
|
+
guest_memory.clone(),
|
|
127
|
+
&store,
|
|
128
|
+
instance.clone(),
|
|
129
|
+
None,
|
|
130
|
+
));
|
|
131
|
+
sandbox
|
|
132
|
+
.initialize_handles_and_layout(&mut store, instance.clone(), handles, None, true)
|
|
133
|
+
.map_err(|error| {
|
|
134
|
+
RunError::Compile(format!("failed to initialize WASI instance: {error}"))
|
|
135
|
+
})?;
|
|
136
|
+
|
|
137
|
+
let start = instance
|
|
138
|
+
.exports
|
|
139
|
+
.get_function("_start")
|
|
140
|
+
.map_err(|error| RunError::Compile(format!("module has no _start function: {error}")))?;
|
|
141
|
+
let execution = if executable.has_deferred_start {
|
|
142
|
+
let initializer = instance
|
|
143
|
+
.exports
|
|
144
|
+
.get_function(DEFERRED_START_EXPORT)
|
|
145
|
+
.map_err(|error| {
|
|
146
|
+
RunError::Runtime(format!("deferred start function is unavailable: {error}"))
|
|
147
|
+
})?;
|
|
148
|
+
match initializer.call(&mut store, &[]) {
|
|
149
|
+
Ok(_) => start.call(&mut store, &[]),
|
|
150
|
+
Err(error) => Err(error),
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
start.call(&mut store, &[])
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let stdout = stdout_capture.bytes();
|
|
157
|
+
let stderr = stderr_capture.bytes();
|
|
158
|
+
let mut output_exceeded = stdout_capture.exceeded() || stderr_capture.exceeded();
|
|
159
|
+
let remaining = remaining_points(&mut store, &meter).map_err(RunError::Runtime)?;
|
|
160
|
+
let logical_time_exceeded = clock.limit_exceeded()?;
|
|
161
|
+
|
|
162
|
+
let mut code = 0;
|
|
163
|
+
let mut termination = ExecutionTermination::Exited;
|
|
164
|
+
let mut trap_message = None;
|
|
165
|
+
if let Err(error) = execution {
|
|
166
|
+
if let Some(wasi_error) = crate::wasi_error(&error) {
|
|
167
|
+
match wasi_error {
|
|
168
|
+
WasiError::Exit(exit) => {
|
|
169
|
+
let errno: wasmer_wasix_types::wasi::Errno = (*exit).into();
|
|
170
|
+
if errno != wasmer_wasix_types::wasi::Errno::Success {
|
|
171
|
+
code = errno as i32;
|
|
172
|
+
termination = ExecutionTermination::Exited;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
WasiError::UnknownWasiVersion => {
|
|
176
|
+
return Err(RunError::WasiUnsupported(
|
|
177
|
+
"unknown WASI version".to_string(),
|
|
178
|
+
));
|
|
179
|
+
}
|
|
180
|
+
WasiError::ThreadExit => {
|
|
181
|
+
return Err(RunError::WasiUnsupported("thread exit".to_string()));
|
|
182
|
+
}
|
|
183
|
+
WasiError::DeepSleep(_) => {
|
|
184
|
+
return Err(RunError::WasiUnsupported("deep sleep".to_string()));
|
|
185
|
+
}
|
|
186
|
+
WasiError::DlSymbolResolutionFailed(symbol) => {
|
|
187
|
+
return Err(RunError::WasiUnsupported(format!(
|
|
188
|
+
"unresolved symbol {symbol}"
|
|
189
|
+
)));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
trap_message = Some(super::canonical_trap_message(&error.to_string()));
|
|
194
|
+
termination = ExecutionTermination::Trap;
|
|
195
|
+
code = 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sandbox.on_exit(
|
|
200
|
+
&mut store,
|
|
201
|
+
Some(wasmer_wasix_types::wasi::Errno::Success.into()),
|
|
202
|
+
);
|
|
203
|
+
let captured_bytes = stdout.len().saturating_add(stderr.len());
|
|
204
|
+
let remaining_output = output_limit.saturating_sub(captured_bytes);
|
|
205
|
+
let (files, file_output_exceeded) =
|
|
206
|
+
read_files_bounded(&filesystem, &request.output_paths, remaining_output)?;
|
|
207
|
+
output_exceeded |= file_output_exceeded;
|
|
208
|
+
|
|
209
|
+
if project_filesystem.quota_exceeded() {
|
|
210
|
+
code = 137;
|
|
211
|
+
termination = ExecutionTermination::FilesystemLimit;
|
|
212
|
+
} else if output_exceeded {
|
|
213
|
+
code = 137;
|
|
214
|
+
termination = ExecutionTermination::OutputLimit;
|
|
215
|
+
} else if logical_time_exceeded {
|
|
216
|
+
code = 137;
|
|
217
|
+
termination = ExecutionTermination::LogicalTimeLimit;
|
|
218
|
+
} else if matches!(remaining, CostPoints::Exhausted) {
|
|
219
|
+
code = 137;
|
|
220
|
+
termination = ExecutionTermination::InstructionLimit;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let memory_bytes = u64::from(guest_memory.size(&store).0) * 65_536;
|
|
224
|
+
if memory_bytes > request.resources.memory_limit_bytes {
|
|
225
|
+
code = 137;
|
|
226
|
+
termination = ExecutionTermination::MemoryLimit;
|
|
227
|
+
}
|
|
228
|
+
if termination != ExecutionTermination::Trap {
|
|
229
|
+
trap_message = None;
|
|
230
|
+
}
|
|
231
|
+
let cost = match remaining {
|
|
232
|
+
CostPoints::Remaining(points) => {
|
|
233
|
+
request.resources.instruction_budget.saturating_sub(points)
|
|
234
|
+
}
|
|
235
|
+
CostPoints::Exhausted => request.resources.instruction_budget,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let filesystem_metrics = project_filesystem.metrics();
|
|
239
|
+
Ok(RunResult {
|
|
240
|
+
code,
|
|
241
|
+
metrics: ExecutionMetrics {
|
|
242
|
+
cost,
|
|
243
|
+
cost_model: METER_MODEL.to_string(),
|
|
244
|
+
operations: metered.operations,
|
|
245
|
+
memory_bytes,
|
|
246
|
+
logical_time_ns: clock.elapsed_ns()?,
|
|
247
|
+
filesystem_bytes: filesystem_metrics.bytes,
|
|
248
|
+
filesystem_entries: filesystem_metrics.entries,
|
|
249
|
+
stdout_bytes: stdout.len() as u64,
|
|
250
|
+
stderr_bytes: stderr.len() as u64,
|
|
251
|
+
},
|
|
252
|
+
stdout,
|
|
253
|
+
stderr,
|
|
254
|
+
files,
|
|
255
|
+
termination,
|
|
256
|
+
trap_message,
|
|
257
|
+
determinism: request.determinism,
|
|
258
|
+
resources: request.resources,
|
|
259
|
+
})
|
|
260
|
+
}
|