@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,1539 @@
|
|
|
1
|
+
//! Pure static admission for immutable `WOJJDG02` judge packages.
|
|
2
|
+
//!
|
|
3
|
+
//! This module deliberately does not compile, instantiate, or execute trusted
|
|
4
|
+
//! judge WebAssembly. It verifies the transport and command ABI before runtime
|
|
5
|
+
//! code is allowed to consume the bytes.
|
|
6
|
+
|
|
7
|
+
use serde::Deserialize;
|
|
8
|
+
use serde::Deserializer;
|
|
9
|
+
use serde_json::Value;
|
|
10
|
+
use sha2::{Digest, Sha256};
|
|
11
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
12
|
+
use thiserror::Error;
|
|
13
|
+
use wasmparser::{Validator, WasmFeatures};
|
|
14
|
+
|
|
15
|
+
use crate::contract::WASM_OJ_CONTRACT_VERSION;
|
|
16
|
+
|
|
17
|
+
pub const WASM_OJ_JUDGE_PACKAGE_MAGIC: &[u8; 8] = b"WOJJDG02";
|
|
18
|
+
pub const WASM_OJ_JUDGE_PACKAGE_SCHEMA: &str = "wasm-oj-v2/judge-package";
|
|
19
|
+
pub const WASM_OJ_JUDGE_PACKAGE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
|
20
|
+
pub const TRUSTED_JUDGE_WASM_MAX_BYTES: usize = 8 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
const HEADER_BYTES: usize = 8 + 4 + 4;
|
|
23
|
+
const BLOB_HEADER_BYTES: usize = 32 + 8;
|
|
24
|
+
const MAX_MANIFEST_BYTES: usize = 256 * 1024;
|
|
25
|
+
const MAX_BLOBS: usize = 258;
|
|
26
|
+
const MAX_ASSETS: usize = 256;
|
|
27
|
+
const MAX_ASSET_BYTES: usize = 4 * 1024 * 1024;
|
|
28
|
+
const MAX_ASSET_TOTAL_BYTES: usize = 4 * 1024 * 1024;
|
|
29
|
+
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
|
|
30
|
+
const MAX_MEMORY_LIMIT_BYTES: u64 = 4 * 1024 * 1024 * 1024;
|
|
31
|
+
const MAX_LOGICAL_TIME_LIMIT_MS: u64 = MAX_SAFE_INTEGER / 1_000_000;
|
|
32
|
+
const MAX_WALL_TIME_LIMIT_MS: u64 = 10 * 60 * 1_000;
|
|
33
|
+
const WASM_PAGE_BYTES: u64 = 65_536;
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
|
36
|
+
#[error("{0}")]
|
|
37
|
+
pub struct JudgePackageError(String);
|
|
38
|
+
|
|
39
|
+
fn invalid(message: impl Into<String>) -> JudgePackageError {
|
|
40
|
+
JudgePackageError(message.into())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Publication metadata that can be checked together with the package bytes.
|
|
44
|
+
#[derive(Debug, Default, Clone, Copy)]
|
|
45
|
+
pub struct JudgePackageValidationOptions<'a> {
|
|
46
|
+
pub expected_bytes: Option<usize>,
|
|
47
|
+
pub expected_sha256: Option<&'a str>,
|
|
48
|
+
pub memory_limit_bytes: Option<u64>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Bounded, execution-relevant manifest projection returned after validation.
|
|
52
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
53
|
+
pub struct JudgePackageManifest {
|
|
54
|
+
pub judge_kind: String,
|
|
55
|
+
pub judge_data_sha256: String,
|
|
56
|
+
pub judge_data_bytes: usize,
|
|
57
|
+
pub allowed_languages: Vec<String>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
61
|
+
pub struct ValidatedJudgePackage {
|
|
62
|
+
pub manifest: JudgePackageManifest,
|
|
63
|
+
pub judge_data_case_count: usize,
|
|
64
|
+
pub bytes: usize,
|
|
65
|
+
pub execution_semantic_sha256: String,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
69
|
+
pub struct TrustedJudgeWasmInfo {
|
|
70
|
+
pub bytes: usize,
|
|
71
|
+
pub initial_memory_pages: u64,
|
|
72
|
+
pub maximum_memory_pages: Option<u64>,
|
|
73
|
+
pub imports: Vec<String>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(Debug, Deserialize)]
|
|
77
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
78
|
+
struct ManifestWire {
|
|
79
|
+
schema: String,
|
|
80
|
+
wasm_oj_contract: u32,
|
|
81
|
+
judge_data: BlobReference,
|
|
82
|
+
allowed_profiles: BTreeMap<String, CompileProfile>,
|
|
83
|
+
judge: JudgeWire,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
87
|
+
#[serde(deny_unknown_fields)]
|
|
88
|
+
struct BlobReference {
|
|
89
|
+
bytes: u64,
|
|
90
|
+
sha256: String,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
94
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
95
|
+
struct AssetReference {
|
|
96
|
+
guest_path: String,
|
|
97
|
+
bytes: u64,
|
|
98
|
+
sha256: String,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[derive(Debug, Deserialize)]
|
|
102
|
+
#[serde(deny_unknown_fields)]
|
|
103
|
+
struct CompileProfile {
|
|
104
|
+
target: String,
|
|
105
|
+
optimization: String,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[derive(Debug, Deserialize)]
|
|
109
|
+
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
|
110
|
+
enum JudgeWire {
|
|
111
|
+
#[serde(rename = "text")]
|
|
112
|
+
Text,
|
|
113
|
+
#[serde(rename = "checker")]
|
|
114
|
+
Checker {
|
|
115
|
+
#[serde(rename = "runtimeProfile")]
|
|
116
|
+
runtime_profile: String,
|
|
117
|
+
artifact: BlobReference,
|
|
118
|
+
assets: Vec<AssetReference>,
|
|
119
|
+
args: Vec<String>,
|
|
120
|
+
},
|
|
121
|
+
#[serde(rename = "interactive")]
|
|
122
|
+
Interactive {
|
|
123
|
+
#[serde(rename = "runtimeProfile")]
|
|
124
|
+
runtime_profile: String,
|
|
125
|
+
artifact: BlobReference,
|
|
126
|
+
assets: Vec<AssetReference>,
|
|
127
|
+
args: Vec<String>,
|
|
128
|
+
#[serde(rename = "inputPath")]
|
|
129
|
+
input_path: String,
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[derive(Debug, Deserialize)]
|
|
134
|
+
#[serde(deny_unknown_fields)]
|
|
135
|
+
struct JudgeDataWire {
|
|
136
|
+
schema: String,
|
|
137
|
+
cases: Vec<JudgeCaseWire>,
|
|
138
|
+
scoring: ScoringWire,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[derive(Debug, Deserialize)]
|
|
142
|
+
#[serde(deny_unknown_fields)]
|
|
143
|
+
struct JudgeCaseWire {
|
|
144
|
+
id: String,
|
|
145
|
+
input: String,
|
|
146
|
+
output: String,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[derive(Debug, Deserialize)]
|
|
150
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
151
|
+
struct ScoringWire {
|
|
152
|
+
maximum_points: u64,
|
|
153
|
+
calibration: CalibrationWire,
|
|
154
|
+
policies: Vec<PolicyWire>,
|
|
155
|
+
safety_limits: SafetyLimitsWire,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[derive(Debug, Deserialize)]
|
|
159
|
+
#[serde(deny_unknown_fields)]
|
|
160
|
+
struct CalibrationWire {
|
|
161
|
+
method: String,
|
|
162
|
+
profiles: BTreeMap<String, String>,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[derive(Debug, Deserialize)]
|
|
166
|
+
#[serde(deny_unknown_fields)]
|
|
167
|
+
struct PolicyWire {
|
|
168
|
+
id: String,
|
|
169
|
+
points: u64,
|
|
170
|
+
limits: LimitsWire,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[derive(Debug, Deserialize)]
|
|
174
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
175
|
+
struct LimitsWire {
|
|
176
|
+
instruction_budget: u64,
|
|
177
|
+
memory_limit_bytes: u64,
|
|
178
|
+
#[serde(default, deserialize_with = "deserialize_present_u64")]
|
|
179
|
+
logical_time_limit_ms: Option<u64>,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn deserialize_present_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
|
183
|
+
where
|
|
184
|
+
D: Deserializer<'de>,
|
|
185
|
+
{
|
|
186
|
+
u64::deserialize(deserializer).map(Some)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[derive(Debug, Deserialize)]
|
|
190
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
191
|
+
struct SafetyLimitsWire {
|
|
192
|
+
wall_time_limit_ms: u64,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[derive(Debug)]
|
|
196
|
+
struct CheckedJudgeData {
|
|
197
|
+
case_count: usize,
|
|
198
|
+
maximum_memory_bytes: u64,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[derive(Debug)]
|
|
202
|
+
struct CheckedManifest {
|
|
203
|
+
wire: ManifestWire,
|
|
204
|
+
references: BTreeMap<[u8; 32], usize>,
|
|
205
|
+
ordered_digests: Vec<[u8; 32]>,
|
|
206
|
+
artifact_digest: Option<[u8; 32]>,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
pub fn validate_judge_package(bytes: &[u8]) -> Result<ValidatedJudgePackage, JudgePackageError> {
|
|
210
|
+
validate_judge_package_with_options(bytes, JudgePackageValidationOptions::default())
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Validate exact `WOJJDG02` bytes without invoking any guest code.
|
|
214
|
+
pub fn validate_judge_package_with_options(
|
|
215
|
+
bytes: &[u8],
|
|
216
|
+
options: JudgePackageValidationOptions<'_>,
|
|
217
|
+
) -> Result<ValidatedJudgePackage, JudgePackageError> {
|
|
218
|
+
if bytes.len() < HEADER_BYTES || bytes.len() > WASM_OJ_JUDGE_PACKAGE_MAX_BYTES {
|
|
219
|
+
return Err(invalid(
|
|
220
|
+
"judge package bytes are outside the 32 MiB transport limit",
|
|
221
|
+
));
|
|
222
|
+
}
|
|
223
|
+
if let Some(expected) = options.expected_bytes
|
|
224
|
+
&& (expected == 0 || expected > WASM_OJ_JUDGE_PACKAGE_MAX_BYTES)
|
|
225
|
+
{
|
|
226
|
+
return Err(invalid("expected judge package byte length is invalid"));
|
|
227
|
+
}
|
|
228
|
+
let expected_digest = options
|
|
229
|
+
.expected_sha256
|
|
230
|
+
.map(parse_digest)
|
|
231
|
+
.transpose()
|
|
232
|
+
.map_err(|_| invalid("expected judge package digest must be lowercase SHA-256"))?;
|
|
233
|
+
|
|
234
|
+
if &bytes[..8] != WASM_OJ_JUDGE_PACKAGE_MAGIC {
|
|
235
|
+
return Err(invalid("judge package transport magic is invalid"));
|
|
236
|
+
}
|
|
237
|
+
let manifest_length =
|
|
238
|
+
u32::from_be_bytes(bytes[8..12].try_into().expect("fixed header")) as usize;
|
|
239
|
+
let blob_count = u32::from_be_bytes(bytes[12..16].try_into().expect("fixed header")) as usize;
|
|
240
|
+
if manifest_length == 0 || manifest_length > MAX_MANIFEST_BYTES || blob_count > MAX_BLOBS {
|
|
241
|
+
return Err(invalid("judge package transport header exceeds its limits"));
|
|
242
|
+
}
|
|
243
|
+
let manifest_end = HEADER_BYTES
|
|
244
|
+
.checked_add(manifest_length)
|
|
245
|
+
.filter(|end| *end <= bytes.len())
|
|
246
|
+
.ok_or_else(|| invalid("judge package manifest is truncated"))?;
|
|
247
|
+
let checked_manifest = check_manifest(&bytes[HEADER_BYTES..manifest_end])?;
|
|
248
|
+
if blob_count != checked_manifest.ordered_digests.len() {
|
|
249
|
+
return Err(invalid(
|
|
250
|
+
"judge package blob count disagrees with its manifest",
|
|
251
|
+
));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let mut cursor = manifest_end;
|
|
255
|
+
let mut checked_judge_data = None;
|
|
256
|
+
for expected_digest_value in &checked_manifest.ordered_digests {
|
|
257
|
+
let header_end = cursor
|
|
258
|
+
.checked_add(BLOB_HEADER_BYTES)
|
|
259
|
+
.filter(|end| *end <= bytes.len())
|
|
260
|
+
.ok_or_else(|| invalid("judge package transport is truncated"))?;
|
|
261
|
+
let actual_digest: [u8; 32] = bytes[cursor..cursor + 32].try_into().expect("fixed digest");
|
|
262
|
+
let blob_length_u64 = u64::from_be_bytes(
|
|
263
|
+
bytes[cursor + 32..header_end]
|
|
264
|
+
.try_into()
|
|
265
|
+
.expect("fixed length"),
|
|
266
|
+
);
|
|
267
|
+
let blob_length = usize::try_from(blob_length_u64)
|
|
268
|
+
.map_err(|_| invalid("judge package blob length exceeds the platform range"))?;
|
|
269
|
+
let expected_length = checked_manifest.references[expected_digest_value];
|
|
270
|
+
if actual_digest != *expected_digest_value || blob_length != expected_length {
|
|
271
|
+
return Err(invalid(
|
|
272
|
+
"judge package blob header disagrees with its manifest",
|
|
273
|
+
));
|
|
274
|
+
}
|
|
275
|
+
cursor = header_end;
|
|
276
|
+
let content_end = cursor
|
|
277
|
+
.checked_add(blob_length)
|
|
278
|
+
.filter(|end| *end <= bytes.len())
|
|
279
|
+
.ok_or_else(|| invalid("judge package transport is truncated"))?;
|
|
280
|
+
let contents = &bytes[cursor..content_end];
|
|
281
|
+
if sha256(contents) != *expected_digest_value {
|
|
282
|
+
return Err(invalid(format!(
|
|
283
|
+
"judge package blob '{}' failed integrity verification",
|
|
284
|
+
hex_digest(expected_digest_value)
|
|
285
|
+
)));
|
|
286
|
+
}
|
|
287
|
+
if *expected_digest_value
|
|
288
|
+
== parse_digest(&checked_manifest.wire.judge_data.sha256)
|
|
289
|
+
.expect("manifest digest was checked")
|
|
290
|
+
{
|
|
291
|
+
checked_judge_data = Some(check_judge_data(
|
|
292
|
+
contents,
|
|
293
|
+
&checked_manifest.wire.allowed_profiles,
|
|
294
|
+
)?);
|
|
295
|
+
}
|
|
296
|
+
if checked_manifest.artifact_digest == Some(*expected_digest_value) {
|
|
297
|
+
let judge_data = checked_judge_data.as_ref().ok_or_else(|| {
|
|
298
|
+
invalid("judge package judgeData must precede its executable blobs")
|
|
299
|
+
})?;
|
|
300
|
+
let memory_limit = options
|
|
301
|
+
.memory_limit_bytes
|
|
302
|
+
.map(|limit| limit.min(judge_data.maximum_memory_bytes))
|
|
303
|
+
.unwrap_or(judge_data.maximum_memory_bytes);
|
|
304
|
+
validate_trusted_judge_wasm(contents, Some(memory_limit))?;
|
|
305
|
+
}
|
|
306
|
+
cursor = content_end;
|
|
307
|
+
}
|
|
308
|
+
if cursor != bytes.len() {
|
|
309
|
+
return Err(invalid("judge package transport contains trailing bytes"));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let package_digest = sha256(bytes);
|
|
313
|
+
if options
|
|
314
|
+
.expected_bytes
|
|
315
|
+
.is_some_and(|expected| expected != bytes.len())
|
|
316
|
+
{
|
|
317
|
+
return Err(invalid(
|
|
318
|
+
"judge package byte length disagrees with its publication",
|
|
319
|
+
));
|
|
320
|
+
}
|
|
321
|
+
if expected_digest.is_some_and(|expected| expected != package_digest) {
|
|
322
|
+
return Err(invalid(
|
|
323
|
+
"judge package digest disagrees with its publication",
|
|
324
|
+
));
|
|
325
|
+
}
|
|
326
|
+
let judge_data =
|
|
327
|
+
checked_judge_data.ok_or_else(|| invalid("judge package is missing its judgeData blob"))?;
|
|
328
|
+
let manifest = manifest_projection(&checked_manifest.wire);
|
|
329
|
+
Ok(ValidatedJudgePackage {
|
|
330
|
+
manifest,
|
|
331
|
+
judge_data_case_count: judge_data.case_count,
|
|
332
|
+
bytes: bytes.len(),
|
|
333
|
+
execution_semantic_sha256: hex_digest(&package_digest),
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn check_manifest(bytes: &[u8]) -> Result<CheckedManifest, JudgePackageError> {
|
|
338
|
+
let wire: ManifestWire = parse_canonical_json(bytes, "judge package manifest")?;
|
|
339
|
+
if wire.schema != WASM_OJ_JUDGE_PACKAGE_SCHEMA
|
|
340
|
+
|| wire.wasm_oj_contract != WASM_OJ_CONTRACT_VERSION
|
|
341
|
+
{
|
|
342
|
+
return Err(invalid("judge package manifest contract is unsupported"));
|
|
343
|
+
}
|
|
344
|
+
check_allowed_profiles(&wire.allowed_profiles)?;
|
|
345
|
+
|
|
346
|
+
let judge_data_digest = check_blob_reference(&wire.judge_data, None, "judgeData")?;
|
|
347
|
+
let mut references = BTreeMap::new();
|
|
348
|
+
references.insert(judge_data_digest, wire.judge_data.bytes as usize);
|
|
349
|
+
let mut artifact_digest = None;
|
|
350
|
+
match &wire.judge {
|
|
351
|
+
JudgeWire::Text => {}
|
|
352
|
+
JudgeWire::Checker {
|
|
353
|
+
runtime_profile,
|
|
354
|
+
artifact,
|
|
355
|
+
assets,
|
|
356
|
+
args,
|
|
357
|
+
} => {
|
|
358
|
+
artifact_digest = Some(check_executable_manifest(
|
|
359
|
+
"checker",
|
|
360
|
+
runtime_profile,
|
|
361
|
+
artifact,
|
|
362
|
+
assets,
|
|
363
|
+
args,
|
|
364
|
+
None,
|
|
365
|
+
&mut references,
|
|
366
|
+
)?);
|
|
367
|
+
}
|
|
368
|
+
JudgeWire::Interactive {
|
|
369
|
+
runtime_profile,
|
|
370
|
+
artifact,
|
|
371
|
+
assets,
|
|
372
|
+
args,
|
|
373
|
+
input_path,
|
|
374
|
+
} => {
|
|
375
|
+
artifact_digest = Some(check_executable_manifest(
|
|
376
|
+
"interactive",
|
|
377
|
+
runtime_profile,
|
|
378
|
+
artifact,
|
|
379
|
+
assets,
|
|
380
|
+
args,
|
|
381
|
+
Some(input_path),
|
|
382
|
+
&mut references,
|
|
383
|
+
)?);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
let ordered_digests = std::iter::once(judge_data_digest)
|
|
387
|
+
.chain(
|
|
388
|
+
references
|
|
389
|
+
.keys()
|
|
390
|
+
.copied()
|
|
391
|
+
.filter(|digest| *digest != judge_data_digest),
|
|
392
|
+
)
|
|
393
|
+
.collect();
|
|
394
|
+
Ok(CheckedManifest {
|
|
395
|
+
wire,
|
|
396
|
+
references,
|
|
397
|
+
ordered_digests,
|
|
398
|
+
artifact_digest,
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[allow(clippy::too_many_arguments)]
|
|
403
|
+
fn check_executable_manifest(
|
|
404
|
+
kind: &str,
|
|
405
|
+
runtime_profile: &str,
|
|
406
|
+
artifact: &BlobReference,
|
|
407
|
+
assets: &[AssetReference],
|
|
408
|
+
args: &[String],
|
|
409
|
+
input_path: Option<&String>,
|
|
410
|
+
references: &mut BTreeMap<[u8; 32], usize>,
|
|
411
|
+
) -> Result<[u8; 32], JudgePackageError> {
|
|
412
|
+
if !matches!(
|
|
413
|
+
runtime_profile,
|
|
414
|
+
"c-wasip1-release" | "cpp-wasip1-release" | "rust-wasip1-release" | "go-wasip1-release"
|
|
415
|
+
) {
|
|
416
|
+
return Err(invalid(format!(
|
|
417
|
+
"judge package {kind} runtimeProfile is unsupported"
|
|
418
|
+
)));
|
|
419
|
+
}
|
|
420
|
+
let artifact_digest = check_blob_reference(
|
|
421
|
+
artifact,
|
|
422
|
+
Some(TRUSTED_JUDGE_WASM_MAX_BYTES),
|
|
423
|
+
"judge artifact",
|
|
424
|
+
)?;
|
|
425
|
+
insert_reference(references, artifact_digest, artifact.bytes as usize)?;
|
|
426
|
+
if assets.len() > MAX_ASSETS {
|
|
427
|
+
return Err(invalid(format!("judge package {kind} assets are invalid")));
|
|
428
|
+
}
|
|
429
|
+
let namespace = if kind == "checker" {
|
|
430
|
+
"/checker/assets/"
|
|
431
|
+
} else {
|
|
432
|
+
"/interactor/assets/"
|
|
433
|
+
};
|
|
434
|
+
let mut previous_path: Option<&str> = None;
|
|
435
|
+
let mut asset_total = 0usize;
|
|
436
|
+
for asset in assets {
|
|
437
|
+
check_guest_path(&asset.guest_path, namespace)?;
|
|
438
|
+
if previous_path.is_some_and(|previous| {
|
|
439
|
+
utf16_cmp(previous, &asset.guest_path) != std::cmp::Ordering::Less
|
|
440
|
+
}) {
|
|
441
|
+
return Err(invalid(format!(
|
|
442
|
+
"judge package {kind} assets must be unique and sorted by guestPath"
|
|
443
|
+
)));
|
|
444
|
+
}
|
|
445
|
+
previous_path = Some(&asset.guest_path);
|
|
446
|
+
let digest = check_blob_reference(
|
|
447
|
+
&BlobReference {
|
|
448
|
+
bytes: asset.bytes,
|
|
449
|
+
sha256: asset.sha256.clone(),
|
|
450
|
+
},
|
|
451
|
+
Some(MAX_ASSET_BYTES),
|
|
452
|
+
"judge asset",
|
|
453
|
+
)?;
|
|
454
|
+
asset_total = asset_total
|
|
455
|
+
.checked_add(asset.bytes as usize)
|
|
456
|
+
.ok_or_else(|| invalid("judge package asset size overflow"))?;
|
|
457
|
+
insert_reference(references, digest, asset.bytes as usize)?;
|
|
458
|
+
}
|
|
459
|
+
if asset_total > MAX_ASSET_TOTAL_BYTES {
|
|
460
|
+
return Err(invalid(format!("judge package {kind} assets exceed 4 MiB")));
|
|
461
|
+
}
|
|
462
|
+
check_args(args)?;
|
|
463
|
+
if let Some(path) = input_path {
|
|
464
|
+
check_guest_path(path, "/interactor/input/")?;
|
|
465
|
+
}
|
|
466
|
+
Ok(artifact_digest)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
fn insert_reference(
|
|
470
|
+
references: &mut BTreeMap<[u8; 32], usize>,
|
|
471
|
+
digest: [u8; 32],
|
|
472
|
+
bytes: usize,
|
|
473
|
+
) -> Result<(), JudgePackageError> {
|
|
474
|
+
if references
|
|
475
|
+
.insert(digest, bytes)
|
|
476
|
+
.is_some_and(|existing| existing != bytes)
|
|
477
|
+
{
|
|
478
|
+
return Err(invalid(
|
|
479
|
+
"judge package repeats one digest with different lengths",
|
|
480
|
+
));
|
|
481
|
+
}
|
|
482
|
+
Ok(())
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn check_blob_reference(
|
|
486
|
+
reference: &BlobReference,
|
|
487
|
+
maximum: Option<usize>,
|
|
488
|
+
label: &str,
|
|
489
|
+
) -> Result<[u8; 32], JudgePackageError> {
|
|
490
|
+
let bytes = usize::try_from(reference.bytes)
|
|
491
|
+
.map_err(|_| invalid(format!("judge package {label} is outside its byte limit")))?;
|
|
492
|
+
if bytes == 0 || bytes > maximum.unwrap_or(WASM_OJ_JUDGE_PACKAGE_MAX_BYTES) {
|
|
493
|
+
return Err(invalid(format!(
|
|
494
|
+
"judge package {label} is outside its byte limit"
|
|
495
|
+
)));
|
|
496
|
+
}
|
|
497
|
+
parse_digest(&reference.sha256)
|
|
498
|
+
.map_err(|_| invalid(format!("judge package {label} digest is invalid")))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
fn check_allowed_profiles(
|
|
502
|
+
profiles: &BTreeMap<String, CompileProfile>,
|
|
503
|
+
) -> Result<(), JudgePackageError> {
|
|
504
|
+
if profiles.is_empty() {
|
|
505
|
+
return Err(invalid(
|
|
506
|
+
"allowedProfiles must contain at least one compile profile",
|
|
507
|
+
));
|
|
508
|
+
}
|
|
509
|
+
for (language, profile) in profiles {
|
|
510
|
+
if !matches!(
|
|
511
|
+
language.as_str(),
|
|
512
|
+
"c" | "cpp" | "rust" | "python" | "javascript" | "typescript" | "go"
|
|
513
|
+
) {
|
|
514
|
+
return Err(invalid(format!(
|
|
515
|
+
"allowedProfiles language '{language}' is unsupported"
|
|
516
|
+
)));
|
|
517
|
+
}
|
|
518
|
+
if !matches!(profile.target.as_str(), "wasip1" | "wasix")
|
|
519
|
+
|| !matches!(profile.optimization.as_str(), "debug" | "release")
|
|
520
|
+
{
|
|
521
|
+
return Err(invalid(format!(
|
|
522
|
+
"allowedProfiles profile for '{language}' is unsupported"
|
|
523
|
+
)));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
Ok(())
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fn check_args(args: &[String]) -> Result<(), JudgePackageError> {
|
|
530
|
+
if args.len() > 64
|
|
531
|
+
|| args
|
|
532
|
+
.iter()
|
|
533
|
+
.any(|arg| arg.contains('\0') || arg.len() > 4_096)
|
|
534
|
+
{
|
|
535
|
+
return Err(invalid("judge package args must be a bounded string array"));
|
|
536
|
+
}
|
|
537
|
+
Ok(())
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
fn check_guest_path(path: &str, namespace: &str) -> Result<(), JudgePackageError> {
|
|
541
|
+
let bad_component = path
|
|
542
|
+
.split('/')
|
|
543
|
+
.any(|component| matches!(component, "." | ".."));
|
|
544
|
+
if utf16_len(path) > 512
|
|
545
|
+
|| !path.starts_with(namespace)
|
|
546
|
+
|| !path.starts_with('/')
|
|
547
|
+
|| path.ends_with('/')
|
|
548
|
+
|| path.contains("//")
|
|
549
|
+
|| path.contains('\\')
|
|
550
|
+
|| path.contains('\0')
|
|
551
|
+
|| bad_component
|
|
552
|
+
{
|
|
553
|
+
return Err(invalid(format!(
|
|
554
|
+
"judge package guest path must be inside '{namespace}'"
|
|
555
|
+
)));
|
|
556
|
+
}
|
|
557
|
+
Ok(())
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
fn manifest_projection(wire: &ManifestWire) -> JudgePackageManifest {
|
|
561
|
+
JudgePackageManifest {
|
|
562
|
+
judge_kind: match wire.judge {
|
|
563
|
+
JudgeWire::Text => "text",
|
|
564
|
+
JudgeWire::Checker { .. } => "checker",
|
|
565
|
+
JudgeWire::Interactive { .. } => "interactive",
|
|
566
|
+
}
|
|
567
|
+
.to_string(),
|
|
568
|
+
judge_data_sha256: wire.judge_data.sha256.clone(),
|
|
569
|
+
judge_data_bytes: wire.judge_data.bytes as usize,
|
|
570
|
+
allowed_languages: wire.allowed_profiles.keys().cloned().collect(),
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
fn check_judge_data(
|
|
575
|
+
bytes: &[u8],
|
|
576
|
+
allowed_profiles: &BTreeMap<String, CompileProfile>,
|
|
577
|
+
) -> Result<CheckedJudgeData, JudgePackageError> {
|
|
578
|
+
let data: JudgeDataWire = parse_canonical_json(bytes, "judge package judgeData")?;
|
|
579
|
+
if data.schema != "wasm-oj-v2/judge-data" {
|
|
580
|
+
return Err(invalid("judge data schema is unsupported"));
|
|
581
|
+
}
|
|
582
|
+
if data.cases.is_empty() || data.cases.len() > 10_000 {
|
|
583
|
+
return Err(invalid("judge data must contain between 1 and 10000 cases"));
|
|
584
|
+
}
|
|
585
|
+
let mut case_ids = BTreeSet::new();
|
|
586
|
+
for case in &data.cases {
|
|
587
|
+
if !valid_slug(&case.id) || !case_ids.insert(case.id.as_str()) {
|
|
588
|
+
return Err(invalid(
|
|
589
|
+
"judge data contains an invalid or duplicate case id",
|
|
590
|
+
));
|
|
591
|
+
}
|
|
592
|
+
// Reading these fields is intentional: their JSON types are part of the contract.
|
|
593
|
+
let _ = (&case.input, &case.output);
|
|
594
|
+
}
|
|
595
|
+
if data.scoring.maximum_points != 100 {
|
|
596
|
+
return Err(invalid("judge data maximumPoints must be 100"));
|
|
597
|
+
}
|
|
598
|
+
if data.scoring.calibration.method != "wasm-oj-v2/compiled-average-optimal-rounded/v1" {
|
|
599
|
+
return Err(invalid("judge data calibration method is unsupported"));
|
|
600
|
+
}
|
|
601
|
+
if data
|
|
602
|
+
.scoring
|
|
603
|
+
.calibration
|
|
604
|
+
.profiles
|
|
605
|
+
.keys()
|
|
606
|
+
.ne(allowed_profiles.keys())
|
|
607
|
+
{
|
|
608
|
+
return Err(invalid(
|
|
609
|
+
"judge data calibration profiles must exactly match allowedProfiles",
|
|
610
|
+
));
|
|
611
|
+
}
|
|
612
|
+
for profile in data.scoring.calibration.profiles.values() {
|
|
613
|
+
if profile.is_empty() || !is_ecmascript_trimmed(profile) || utf16_len(profile) > 4_096 {
|
|
614
|
+
return Err(invalid("judge data calibration profile is invalid"));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
let policy_ids = ["baseline", "efficient", "optimal"];
|
|
618
|
+
if data.scoring.policies.len() != policy_ids.len() {
|
|
619
|
+
return Err(invalid("judge data scoring policies are invalid"));
|
|
620
|
+
}
|
|
621
|
+
let mut points = 0u64;
|
|
622
|
+
for (index, policy) in data.scoring.policies.iter().enumerate() {
|
|
623
|
+
if policy.id != policy_ids[index] {
|
|
624
|
+
return Err(invalid("judge data policies are not in canonical order"));
|
|
625
|
+
}
|
|
626
|
+
positive_at_most(policy.points, 100, "judge data policy points")?;
|
|
627
|
+
points += policy.points;
|
|
628
|
+
check_limits(&policy.limits)?;
|
|
629
|
+
if index > 0 {
|
|
630
|
+
check_policy_order(&data.scoring.policies[index - 1].limits, &policy.limits)?;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if points != 100 {
|
|
634
|
+
return Err(invalid("judge data policy points must sum to 100"));
|
|
635
|
+
}
|
|
636
|
+
positive_at_most(
|
|
637
|
+
data.scoring.safety_limits.wall_time_limit_ms,
|
|
638
|
+
MAX_WALL_TIME_LIMIT_MS,
|
|
639
|
+
"judge data wallTimeLimitMs",
|
|
640
|
+
)?;
|
|
641
|
+
let maximum_memory_bytes = data
|
|
642
|
+
.scoring
|
|
643
|
+
.policies
|
|
644
|
+
.iter()
|
|
645
|
+
.map(|policy| policy.limits.memory_limit_bytes)
|
|
646
|
+
.max()
|
|
647
|
+
.expect("three policies were checked");
|
|
648
|
+
Ok(CheckedJudgeData {
|
|
649
|
+
case_count: data.cases.len(),
|
|
650
|
+
maximum_memory_bytes,
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fn check_limits(limits: &LimitsWire) -> Result<(), JudgePackageError> {
|
|
655
|
+
positive_at_most(
|
|
656
|
+
limits.instruction_budget,
|
|
657
|
+
MAX_SAFE_INTEGER,
|
|
658
|
+
"judge data instructionBudget",
|
|
659
|
+
)?;
|
|
660
|
+
positive_at_most(
|
|
661
|
+
limits.memory_limit_bytes,
|
|
662
|
+
MAX_MEMORY_LIMIT_BYTES,
|
|
663
|
+
"judge data memoryLimitBytes",
|
|
664
|
+
)?;
|
|
665
|
+
if limits.memory_limit_bytes < WASM_PAGE_BYTES
|
|
666
|
+
|| !limits.memory_limit_bytes.is_multiple_of(WASM_PAGE_BYTES)
|
|
667
|
+
{
|
|
668
|
+
return Err(invalid(
|
|
669
|
+
"judge data memoryLimitBytes must be a positive number of Wasm pages",
|
|
670
|
+
));
|
|
671
|
+
}
|
|
672
|
+
if let Some(logical) = limits.logical_time_limit_ms {
|
|
673
|
+
positive_at_most(
|
|
674
|
+
logical,
|
|
675
|
+
MAX_LOGICAL_TIME_LIMIT_MS,
|
|
676
|
+
"judge data logicalTimeLimitMs",
|
|
677
|
+
)?;
|
|
678
|
+
}
|
|
679
|
+
Ok(())
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
fn check_policy_order(broad: &LimitsWire, strict: &LimitsWire) -> Result<(), JudgePackageError> {
|
|
683
|
+
let logical_invalid = broad.logical_time_limit_ms.is_some_and(|broad_value| {
|
|
684
|
+
strict
|
|
685
|
+
.logical_time_limit_ms
|
|
686
|
+
.is_none_or(|value| value > broad_value)
|
|
687
|
+
});
|
|
688
|
+
let any_stricter = strict.instruction_budget < broad.instruction_budget
|
|
689
|
+
|| strict.memory_limit_bytes < broad.memory_limit_bytes
|
|
690
|
+
|| (broad.logical_time_limit_ms.is_none() && strict.logical_time_limit_ms.is_some())
|
|
691
|
+
|| matches!(
|
|
692
|
+
(broad.logical_time_limit_ms, strict.logical_time_limit_ms),
|
|
693
|
+
(Some(broad_value), Some(strict_value)) if strict_value < broad_value
|
|
694
|
+
);
|
|
695
|
+
if strict.instruction_budget > broad.instruction_budget
|
|
696
|
+
|| strict.memory_limit_bytes > broad.memory_limit_bytes
|
|
697
|
+
|| logical_invalid
|
|
698
|
+
|| !any_stricter
|
|
699
|
+
{
|
|
700
|
+
return Err(invalid(
|
|
701
|
+
"judge data policies must be ordered broad-to-strict",
|
|
702
|
+
));
|
|
703
|
+
}
|
|
704
|
+
Ok(())
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn positive_at_most(value: u64, maximum: u64, label: &str) -> Result<(), JudgePackageError> {
|
|
708
|
+
if value == 0 || value > maximum {
|
|
709
|
+
return Err(invalid(format!("{label} is outside its limit")));
|
|
710
|
+
}
|
|
711
|
+
Ok(())
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
fn valid_slug(value: &str) -> bool {
|
|
715
|
+
!value.is_empty()
|
|
716
|
+
&& value.split('-').all(|part| {
|
|
717
|
+
!part.is_empty()
|
|
718
|
+
&& part
|
|
719
|
+
.bytes()
|
|
720
|
+
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
fn utf16_len(value: &str) -> usize {
|
|
725
|
+
value.encode_utf16().count()
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
fn utf16_cmp(left: &str, right: &str) -> std::cmp::Ordering {
|
|
729
|
+
left.encode_utf16().cmp(right.encode_utf16())
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
fn is_ecmascript_trimmed(value: &str) -> bool {
|
|
733
|
+
value.trim_matches(is_ecmascript_whitespace) == value
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
fn is_ecmascript_whitespace(value: char) -> bool {
|
|
737
|
+
matches!(
|
|
738
|
+
value,
|
|
739
|
+
'\u{0009}'
|
|
740
|
+
| '\u{000a}'
|
|
741
|
+
| '\u{000b}'
|
|
742
|
+
| '\u{000c}'
|
|
743
|
+
| '\u{000d}'
|
|
744
|
+
| '\u{0020}'
|
|
745
|
+
| '\u{00a0}'
|
|
746
|
+
| '\u{1680}'
|
|
747
|
+
| '\u{2000}'
|
|
748
|
+
..='\u{200a}'
|
|
749
|
+
| '\u{2028}'
|
|
750
|
+
| '\u{2029}'
|
|
751
|
+
| '\u{202f}'
|
|
752
|
+
| '\u{205f}'
|
|
753
|
+
| '\u{3000}'
|
|
754
|
+
| '\u{feff}'
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fn parse_canonical_json<T: for<'de> Deserialize<'de>>(
|
|
759
|
+
bytes: &[u8],
|
|
760
|
+
label: &str,
|
|
761
|
+
) -> Result<T, JudgePackageError> {
|
|
762
|
+
let value: Value = serde_json::from_slice(bytes)
|
|
763
|
+
.map_err(|error| invalid(format!("{label} is not valid UTF-8 JSON: {error}")))?;
|
|
764
|
+
check_safe_json_numbers(&value, label)?;
|
|
765
|
+
let mut canonical = serde_json::to_vec(&value)
|
|
766
|
+
.map_err(|error| invalid(format!("failed to canonicalize {label}: {error}")))?;
|
|
767
|
+
canonical.push(b'\n');
|
|
768
|
+
if canonical != bytes {
|
|
769
|
+
return Err(invalid(format!(
|
|
770
|
+
"{label} is not encoded as WASM-OJ canonical JSON"
|
|
771
|
+
)));
|
|
772
|
+
}
|
|
773
|
+
serde_json::from_value(value)
|
|
774
|
+
.map_err(|error| invalid(format!("{label} has an invalid shape: {error}")))
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
fn check_safe_json_numbers(value: &Value, label: &str) -> Result<(), JudgePackageError> {
|
|
778
|
+
match value {
|
|
779
|
+
Value::Number(number) => {
|
|
780
|
+
let valid = number
|
|
781
|
+
.as_u64()
|
|
782
|
+
.is_some_and(|integer| integer <= MAX_SAFE_INTEGER)
|
|
783
|
+
|| number.as_i64().is_some_and(|integer| {
|
|
784
|
+
integer >= -(MAX_SAFE_INTEGER as i64) && integer <= MAX_SAFE_INTEGER as i64
|
|
785
|
+
});
|
|
786
|
+
if !valid {
|
|
787
|
+
return Err(invalid(format!("{label} contains a non-canonical number")));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
Value::Array(values) => {
|
|
791
|
+
for item in values {
|
|
792
|
+
check_safe_json_numbers(item, label)?;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
Value::Object(object) => {
|
|
796
|
+
for item in object.values() {
|
|
797
|
+
check_safe_json_numbers(item, label)?;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
Value::Null | Value::Bool(_) | Value::String(_) => {}
|
|
801
|
+
}
|
|
802
|
+
Ok(())
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
fn parse_digest(value: &str) -> Result<[u8; 32], JudgePackageError> {
|
|
806
|
+
if value.len() != 64
|
|
807
|
+
|| !value
|
|
808
|
+
.bytes()
|
|
809
|
+
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
|
810
|
+
{
|
|
811
|
+
return Err(invalid("value is not a lowercase SHA-256 digest"));
|
|
812
|
+
}
|
|
813
|
+
let mut digest = [0u8; 32];
|
|
814
|
+
for (index, output) in digest.iter_mut().enumerate() {
|
|
815
|
+
let offset = index * 2;
|
|
816
|
+
*output = (hex_nibble(value.as_bytes()[offset])? << 4)
|
|
817
|
+
| hex_nibble(value.as_bytes()[offset + 1])?;
|
|
818
|
+
}
|
|
819
|
+
Ok(digest)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
fn hex_nibble(value: u8) -> Result<u8, JudgePackageError> {
|
|
823
|
+
match value {
|
|
824
|
+
b'0'..=b'9' => Ok(value - b'0'),
|
|
825
|
+
b'a'..=b'f' => Ok(value - b'a' + 10),
|
|
826
|
+
_ => Err(invalid("invalid lowercase hexadecimal digit")),
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
fn sha256(bytes: &[u8]) -> [u8; 32] {
|
|
831
|
+
Sha256::digest(bytes).into()
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
fn hex_digest(digest: &[u8; 32]) -> String {
|
|
835
|
+
const HEX: &[u8; 16] = b"0123456789abcdef";
|
|
836
|
+
let mut result = String::with_capacity(64);
|
|
837
|
+
for byte in digest {
|
|
838
|
+
result.push(HEX[(byte >> 4) as usize] as char);
|
|
839
|
+
result.push(HEX[(byte & 0x0f) as usize] as char);
|
|
840
|
+
}
|
|
841
|
+
result
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
845
|
+
struct FunctionType {
|
|
846
|
+
parameters: Vec<u8>,
|
|
847
|
+
results: Vec<u8>,
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
851
|
+
struct ExportEntry {
|
|
852
|
+
name: String,
|
|
853
|
+
kind: u8,
|
|
854
|
+
index: u32,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
struct WasmReader<'a> {
|
|
858
|
+
bytes: &'a [u8],
|
|
859
|
+
offset: usize,
|
|
860
|
+
label: String,
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
impl<'a> WasmReader<'a> {
|
|
864
|
+
fn new(bytes: &'a [u8], label: impl Into<String>) -> Self {
|
|
865
|
+
Self {
|
|
866
|
+
bytes,
|
|
867
|
+
offset: 0,
|
|
868
|
+
label: label.into(),
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
fn done(&self) -> bool {
|
|
873
|
+
self.offset == self.bytes.len()
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
fn require_done(&self) -> Result<(), JudgePackageError> {
|
|
877
|
+
if self.done() {
|
|
878
|
+
Ok(())
|
|
879
|
+
} else {
|
|
880
|
+
Err(invalid(format!("{} has trailing bytes", self.label)))
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
fn byte(&mut self) -> Result<u8, JudgePackageError> {
|
|
885
|
+
let byte = self
|
|
886
|
+
.bytes
|
|
887
|
+
.get(self.offset)
|
|
888
|
+
.copied()
|
|
889
|
+
.ok_or_else(|| invalid(format!("{} is truncated", self.label)))?;
|
|
890
|
+
self.offset += 1;
|
|
891
|
+
Ok(byte)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
fn u32(&mut self) -> Result<u32, JudgePackageError> {
|
|
895
|
+
let mut value = 0u32;
|
|
896
|
+
for index in 0..5 {
|
|
897
|
+
let byte = self.byte()?;
|
|
898
|
+
if index == 4 && byte & 0xf0 != 0 {
|
|
899
|
+
return Err(invalid(format!(
|
|
900
|
+
"{} contains an overflowing varuint32",
|
|
901
|
+
self.label
|
|
902
|
+
)));
|
|
903
|
+
}
|
|
904
|
+
value |= u32::from(byte & 0x7f) << (index * 7);
|
|
905
|
+
if byte & 0x80 == 0 {
|
|
906
|
+
return Ok(value);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
Err(invalid(format!(
|
|
910
|
+
"{} contains an invalid varuint32",
|
|
911
|
+
self.label
|
|
912
|
+
)))
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
fn take(&mut self, length: usize) -> Result<&'a [u8], JudgePackageError> {
|
|
916
|
+
let end = self
|
|
917
|
+
.offset
|
|
918
|
+
.checked_add(length)
|
|
919
|
+
.filter(|end| *end <= self.bytes.len())
|
|
920
|
+
.ok_or_else(|| invalid(format!("{} is truncated", self.label)))?;
|
|
921
|
+
let result = &self.bytes[self.offset..end];
|
|
922
|
+
self.offset = end;
|
|
923
|
+
Ok(result)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
fn section(
|
|
927
|
+
&mut self,
|
|
928
|
+
length: usize,
|
|
929
|
+
label: impl Into<String>,
|
|
930
|
+
) -> Result<WasmReader<'a>, JudgePackageError> {
|
|
931
|
+
Ok(WasmReader::new(self.take(length)?, label))
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
fn name(&mut self) -> Result<String, JudgePackageError> {
|
|
935
|
+
let length = self.u32()? as usize;
|
|
936
|
+
if length > 4_096 {
|
|
937
|
+
return Err(invalid(format!(
|
|
938
|
+
"{} contains an invalid name length",
|
|
939
|
+
self.label
|
|
940
|
+
)));
|
|
941
|
+
}
|
|
942
|
+
std::str::from_utf8(self.take(length)?)
|
|
943
|
+
.map(str::to_owned)
|
|
944
|
+
.map_err(|_| invalid(format!("{} contains a non-UTF-8 name", self.label)))
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
fn vector<T>(
|
|
948
|
+
&mut self,
|
|
949
|
+
mut read: impl FnMut(&mut Self, usize) -> Result<T, JudgePackageError>,
|
|
950
|
+
) -> Result<Vec<T>, JudgePackageError> {
|
|
951
|
+
let count = self.u32()? as usize;
|
|
952
|
+
let mut result = Vec::with_capacity(count.min(self.bytes.len()));
|
|
953
|
+
for index in 0..count {
|
|
954
|
+
result.push(read(self, index)?);
|
|
955
|
+
}
|
|
956
|
+
Ok(result)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/// Validate the trusted checker/interactor command ABI without instantiation.
|
|
961
|
+
pub fn validate_trusted_judge_wasm(
|
|
962
|
+
bytes: &[u8],
|
|
963
|
+
memory_limit_bytes: Option<u64>,
|
|
964
|
+
) -> Result<TrustedJudgeWasmInfo, JudgePackageError> {
|
|
965
|
+
if bytes.len() < 8 || bytes.len() > TRUSTED_JUDGE_WASM_MAX_BYTES {
|
|
966
|
+
return Err(invalid(
|
|
967
|
+
"trusted judge Wasm is outside the 8 MiB artifact limit",
|
|
968
|
+
));
|
|
969
|
+
}
|
|
970
|
+
if bytes[..8] != [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00] {
|
|
971
|
+
return Err(invalid(
|
|
972
|
+
"trusted judge artifact is not a core WebAssembly v1 module",
|
|
973
|
+
));
|
|
974
|
+
}
|
|
975
|
+
if let Some(limit) = memory_limit_bytes
|
|
976
|
+
&& (limit < WASM_PAGE_BYTES || !limit.is_multiple_of(WASM_PAGE_BYTES))
|
|
977
|
+
{
|
|
978
|
+
return Err(invalid(
|
|
979
|
+
"trusted judge memory limit must be a positive multiple of 64 KiB",
|
|
980
|
+
));
|
|
981
|
+
}
|
|
982
|
+
let mut features = WasmFeatures::default();
|
|
983
|
+
features.remove(
|
|
984
|
+
WasmFeatures::THREADS
|
|
985
|
+
| WasmFeatures::SHARED_EVERYTHING_THREADS
|
|
986
|
+
| WasmFeatures::MEMORY64
|
|
987
|
+
| WasmFeatures::MULTI_MEMORY
|
|
988
|
+
| WasmFeatures::COMPONENT_MODEL
|
|
989
|
+
| WasmFeatures::CUSTOM_PAGE_SIZES,
|
|
990
|
+
);
|
|
991
|
+
Validator::new_with_features(features)
|
|
992
|
+
.validate_all(bytes)
|
|
993
|
+
.map_err(|error| {
|
|
994
|
+
invalid(format!(
|
|
995
|
+
"trusted judge artifact is not valid WebAssembly: {error}"
|
|
996
|
+
))
|
|
997
|
+
})?;
|
|
998
|
+
|
|
999
|
+
let mut reader = WasmReader::new(&bytes[8..], "trusted judge module");
|
|
1000
|
+
let mut types = Vec::new();
|
|
1001
|
+
let mut imported_types = Vec::new();
|
|
1002
|
+
let mut import_names = Vec::new();
|
|
1003
|
+
let mut defined_types = Vec::new();
|
|
1004
|
+
let mut memory = None;
|
|
1005
|
+
let mut exports = Vec::new();
|
|
1006
|
+
while !reader.done() {
|
|
1007
|
+
let id = reader.byte()?;
|
|
1008
|
+
let length = reader.u32()? as usize;
|
|
1009
|
+
let mut section = reader.section(length, format!("trusted judge section {id}"))?;
|
|
1010
|
+
match id {
|
|
1011
|
+
0 => {}
|
|
1012
|
+
1 => types = parse_wasm_types(&mut section)?,
|
|
1013
|
+
2 => {
|
|
1014
|
+
(imported_types, import_names) = parse_wasm_imports(&mut section)?;
|
|
1015
|
+
}
|
|
1016
|
+
3 => {
|
|
1017
|
+
defined_types = section.vector(|reader, _| reader.u32())?;
|
|
1018
|
+
section.require_done()?;
|
|
1019
|
+
}
|
|
1020
|
+
5 => memory = Some(parse_wasm_memory(&mut section)?),
|
|
1021
|
+
7 => {
|
|
1022
|
+
exports = section.vector(|reader, _| {
|
|
1023
|
+
Ok(ExportEntry {
|
|
1024
|
+
name: reader.name()?,
|
|
1025
|
+
kind: reader.byte()?,
|
|
1026
|
+
index: reader.u32()?,
|
|
1027
|
+
})
|
|
1028
|
+
})?;
|
|
1029
|
+
section.require_done()?;
|
|
1030
|
+
}
|
|
1031
|
+
8 => {
|
|
1032
|
+
return Err(invalid(
|
|
1033
|
+
"trusted judge Wasm must not declare a start section; '_start' is the only entrypoint",
|
|
1034
|
+
));
|
|
1035
|
+
}
|
|
1036
|
+
_ => {}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
let (initial, maximum) = memory
|
|
1040
|
+
.ok_or_else(|| invalid("trusted judge Wasm must define exactly one linear memory"))?;
|
|
1041
|
+
if memory_limit_bytes.is_some_and(|limit| initial.saturating_mul(WASM_PAGE_BYTES) > limit) {
|
|
1042
|
+
return Err(invalid(
|
|
1043
|
+
"trusted judge initial memory exceeds the problem memory limit",
|
|
1044
|
+
));
|
|
1045
|
+
}
|
|
1046
|
+
for export in &exports {
|
|
1047
|
+
if matches!(
|
|
1048
|
+
export.name.as_str(),
|
|
1049
|
+
"gas_counter" | "__wasm_oj_deferred_start"
|
|
1050
|
+
) || export.name.starts_with("__wasm_oj_")
|
|
1051
|
+
{
|
|
1052
|
+
return Err(invalid(format!(
|
|
1053
|
+
"trusted judge export '{}' is reserved by WASM-OJ",
|
|
1054
|
+
export.name
|
|
1055
|
+
)));
|
|
1056
|
+
}
|
|
1057
|
+
if !matches!(export.name.as_str(), "memory" | "_start") {
|
|
1058
|
+
return Err(invalid(format!(
|
|
1059
|
+
"trusted judge export '{}' is outside the admitted command ABI",
|
|
1060
|
+
export.name
|
|
1061
|
+
)));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
let memory_exports: Vec<_> = exports
|
|
1065
|
+
.iter()
|
|
1066
|
+
.filter(|entry| entry.name == "memory")
|
|
1067
|
+
.collect();
|
|
1068
|
+
if memory_exports.len() != 1 || memory_exports[0].kind != 2 || memory_exports[0].index != 0 {
|
|
1069
|
+
return Err(invalid(
|
|
1070
|
+
"trusted judge Wasm must export its sole linear memory as 'memory'",
|
|
1071
|
+
));
|
|
1072
|
+
}
|
|
1073
|
+
let start_exports: Vec<_> = exports
|
|
1074
|
+
.iter()
|
|
1075
|
+
.filter(|entry| entry.name == "_start")
|
|
1076
|
+
.collect();
|
|
1077
|
+
if start_exports.len() != 1 || start_exports[0].kind != 0 {
|
|
1078
|
+
return Err(invalid(
|
|
1079
|
+
"trusted judge Wasm must export exactly one '_start' function",
|
|
1080
|
+
));
|
|
1081
|
+
}
|
|
1082
|
+
let start_index = start_exports[0].index as usize;
|
|
1083
|
+
if start_index < imported_types.len() {
|
|
1084
|
+
return Err(invalid(
|
|
1085
|
+
"trusted judge '_start' must be defined by the module",
|
|
1086
|
+
));
|
|
1087
|
+
}
|
|
1088
|
+
let start_type = defined_types
|
|
1089
|
+
.get(start_index - imported_types.len())
|
|
1090
|
+
.and_then(|index| types.get(*index as usize));
|
|
1091
|
+
if !matches!(start_type, Some(function) if function.parameters.is_empty() && function.results.is_empty())
|
|
1092
|
+
{
|
|
1093
|
+
return Err(invalid(
|
|
1094
|
+
"trusted judge '_start' must have the signature () -> ()",
|
|
1095
|
+
));
|
|
1096
|
+
}
|
|
1097
|
+
for index in imported_types.iter().chain(&defined_types) {
|
|
1098
|
+
if types.get(*index as usize).is_none() {
|
|
1099
|
+
return Err(invalid("trusted judge function refers to a missing type"));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
let unique_imports: BTreeSet<_> = import_names.iter().collect();
|
|
1103
|
+
if unique_imports.len() != import_names.len() {
|
|
1104
|
+
return Err(invalid("trusted judge Wasm repeats a WASI import"));
|
|
1105
|
+
}
|
|
1106
|
+
for (index, qualified_name) in import_names.iter().enumerate() {
|
|
1107
|
+
let name = qualified_name
|
|
1108
|
+
.strip_prefix("wasi_snapshot_preview1.")
|
|
1109
|
+
.expect("import parser checks namespace");
|
|
1110
|
+
let (parameters, results) =
|
|
1111
|
+
wasi_signature(name).expect("import parser checks the admitted name surface");
|
|
1112
|
+
let actual = &types[imported_types[index] as usize];
|
|
1113
|
+
if actual.parameters != parameters || actual.results != results {
|
|
1114
|
+
return Err(invalid(format!(
|
|
1115
|
+
"trusted judge import '{qualified_name}' has an invalid WASI ABI signature"
|
|
1116
|
+
)));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
Ok(TrustedJudgeWasmInfo {
|
|
1120
|
+
bytes: bytes.len(),
|
|
1121
|
+
initial_memory_pages: initial,
|
|
1122
|
+
maximum_memory_pages: maximum,
|
|
1123
|
+
imports: import_names,
|
|
1124
|
+
})
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
fn parse_wasm_types(reader: &mut WasmReader<'_>) -> Result<Vec<FunctionType>, JudgePackageError> {
|
|
1128
|
+
let result = reader.vector(|reader, _| {
|
|
1129
|
+
if reader.byte()? != 0x60 {
|
|
1130
|
+
return Err(invalid(
|
|
1131
|
+
"trusted judge Wasm may declare only core function types",
|
|
1132
|
+
));
|
|
1133
|
+
}
|
|
1134
|
+
let parameters = reader.vector(|reader, _| wasm_value_type(reader))?;
|
|
1135
|
+
let results = reader.vector(|reader, _| wasm_value_type(reader))?;
|
|
1136
|
+
Ok(FunctionType {
|
|
1137
|
+
parameters,
|
|
1138
|
+
results,
|
|
1139
|
+
})
|
|
1140
|
+
})?;
|
|
1141
|
+
reader.require_done()?;
|
|
1142
|
+
Ok(result)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
fn wasm_value_type(reader: &mut WasmReader<'_>) -> Result<u8, JudgePackageError> {
|
|
1146
|
+
let value = reader.byte()?;
|
|
1147
|
+
if matches!(value, 0x7f | 0x7e | 0x7d | 0x7c | 0x7b | 0x70 | 0x6f) {
|
|
1148
|
+
Ok(value)
|
|
1149
|
+
} else {
|
|
1150
|
+
Err(invalid(
|
|
1151
|
+
"trusted judge Wasm uses a type outside the admitted core value-type surface",
|
|
1152
|
+
))
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
fn parse_wasm_imports(
|
|
1157
|
+
reader: &mut WasmReader<'_>,
|
|
1158
|
+
) -> Result<(Vec<u32>, Vec<String>), JudgePackageError> {
|
|
1159
|
+
let imports = reader.vector(|reader, _| {
|
|
1160
|
+
let namespace = reader.name()?;
|
|
1161
|
+
let name = reader.name()?;
|
|
1162
|
+
let kind = reader.byte()?;
|
|
1163
|
+
if kind != 0 {
|
|
1164
|
+
return Err(invalid(format!(
|
|
1165
|
+
"trusted judge import '{namespace}.{name}' must be a function"
|
|
1166
|
+
)));
|
|
1167
|
+
}
|
|
1168
|
+
if namespace != "wasi_snapshot_preview1" || wasi_signature(&name).is_none() {
|
|
1169
|
+
return Err(invalid(format!(
|
|
1170
|
+
"trusted judge import '{namespace}.{name}' is outside the admitted WASI Preview 1 surface"
|
|
1171
|
+
)));
|
|
1172
|
+
}
|
|
1173
|
+
Ok((reader.u32()?, format!("{namespace}.{name}")))
|
|
1174
|
+
})?;
|
|
1175
|
+
reader.require_done()?;
|
|
1176
|
+
Ok(imports.into_iter().unzip())
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
fn parse_wasm_memory(reader: &mut WasmReader<'_>) -> Result<(u64, Option<u64>), JudgePackageError> {
|
|
1180
|
+
let memories = reader.vector(|reader, _| {
|
|
1181
|
+
let flags = reader.u32()?;
|
|
1182
|
+
if !matches!(flags, 0 | 1) {
|
|
1183
|
+
return Err(invalid(
|
|
1184
|
+
"trusted judge memory must be 32-bit, unshared, and use the default page size",
|
|
1185
|
+
));
|
|
1186
|
+
}
|
|
1187
|
+
let initial = u64::from(reader.u32()?);
|
|
1188
|
+
let maximum = if flags == 1 {
|
|
1189
|
+
Some(u64::from(reader.u32()?))
|
|
1190
|
+
} else {
|
|
1191
|
+
None
|
|
1192
|
+
};
|
|
1193
|
+
if maximum.is_some_and(|maximum| maximum < initial) {
|
|
1194
|
+
return Err(invalid(
|
|
1195
|
+
"trusted judge memory maximum is below its initial size",
|
|
1196
|
+
));
|
|
1197
|
+
}
|
|
1198
|
+
Ok((initial, maximum))
|
|
1199
|
+
})?;
|
|
1200
|
+
reader.require_done()?;
|
|
1201
|
+
if memories.len() != 1 {
|
|
1202
|
+
return Err(invalid(
|
|
1203
|
+
"trusted judge Wasm must define exactly one linear memory",
|
|
1204
|
+
));
|
|
1205
|
+
}
|
|
1206
|
+
Ok(memories[0])
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
fn wasi_signature(name: &str) -> Option<(&'static [u8], &'static [u8])> {
|
|
1210
|
+
const I32: u8 = 0x7f;
|
|
1211
|
+
const I64: u8 = 0x7e;
|
|
1212
|
+
let errno: &'static [u8] = &[I32];
|
|
1213
|
+
let signature = match name {
|
|
1214
|
+
"args_get" | "args_sizes_get" | "environ_get" | "environ_sizes_get" | "fd_fdstat_get"
|
|
1215
|
+
| "fd_filestat_get" | "fd_prestat_get" | "fd_tell" | "random_get" => {
|
|
1216
|
+
(&[I32, I32][..], errno)
|
|
1217
|
+
}
|
|
1218
|
+
"clock_res_get" => (&[I32, I32][..], errno),
|
|
1219
|
+
"clock_time_get" => (&[I32, I64, I32][..], errno),
|
|
1220
|
+
"fd_advise" => (&[I32, I64, I64, I32][..], errno),
|
|
1221
|
+
"fd_allocate" => (&[I32, I64, I64][..], errno),
|
|
1222
|
+
"fd_close" | "fd_datasync" | "fd_sync" | "proc_raise" => (&[I32][..], errno),
|
|
1223
|
+
"fd_fdstat_set_flags" => (&[I32, I32][..], errno),
|
|
1224
|
+
"fd_fdstat_set_rights" => (&[I32, I64, I64][..], errno),
|
|
1225
|
+
"fd_filestat_set_size" => (&[I32, I64][..], errno),
|
|
1226
|
+
"fd_filestat_set_times" => (&[I32, I64, I64, I32][..], errno),
|
|
1227
|
+
"fd_pread" | "fd_pwrite" => (&[I32, I32, I32, I64, I32][..], errno),
|
|
1228
|
+
"fd_prestat_dir_name"
|
|
1229
|
+
| "path_create_directory"
|
|
1230
|
+
| "path_remove_directory"
|
|
1231
|
+
| "path_unlink_file" => (&[I32, I32, I32][..], errno),
|
|
1232
|
+
"fd_read" | "fd_write" | "poll_oneoff" => (&[I32, I32, I32, I32][..], errno),
|
|
1233
|
+
"fd_readdir" => (&[I32, I32, I32, I64, I32][..], errno),
|
|
1234
|
+
"fd_renumber" => (&[I32, I32][..], errno),
|
|
1235
|
+
"fd_seek" => (&[I32, I64, I32, I32][..], errno),
|
|
1236
|
+
"path_filestat_get" => (&[I32, I32, I32, I32, I32][..], errno),
|
|
1237
|
+
"path_filestat_set_times" => (&[I32, I32, I32, I32, I64, I64, I32][..], errno),
|
|
1238
|
+
"path_link" => (&[I32, I32, I32, I32, I32, I32, I32][..], errno),
|
|
1239
|
+
"path_rename" => (&[I32, I32, I32, I32, I32, I32][..], errno),
|
|
1240
|
+
"path_open" => (&[I32, I32, I32, I32, I32, I64, I64, I32, I32][..], errno),
|
|
1241
|
+
"path_readlink" => (&[I32, I32, I32, I32, I32, I32][..], errno),
|
|
1242
|
+
"path_symlink" => (&[I32, I32, I32, I32, I32][..], errno),
|
|
1243
|
+
"proc_exit" => (&[I32][..], &[][..]),
|
|
1244
|
+
"sched_yield" => (&[][..], errno),
|
|
1245
|
+
_ => return None,
|
|
1246
|
+
};
|
|
1247
|
+
Some(signature)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
#[cfg(test)]
|
|
1251
|
+
mod tests {
|
|
1252
|
+
use super::*;
|
|
1253
|
+
|
|
1254
|
+
const GOLDEN_HEX: &str = include_str!("../../../testdata/wojjdg02-v2-text.hex");
|
|
1255
|
+
const GOLDEN_DIGEST: &str = "0039034e813284b1a22fa6c11c1351097cb9141e5954f03f1b2bea98a9b5f12e";
|
|
1256
|
+
|
|
1257
|
+
fn decode_hex(value: &str) -> Vec<u8> {
|
|
1258
|
+
let value = value.trim();
|
|
1259
|
+
assert!(value.len().is_multiple_of(2));
|
|
1260
|
+
value
|
|
1261
|
+
.as_bytes()
|
|
1262
|
+
.chunks_exact(2)
|
|
1263
|
+
.map(|pair| (hex_nibble(pair[0]).unwrap() << 4) | hex_nibble(pair[1]).unwrap())
|
|
1264
|
+
.collect()
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
#[test]
|
|
1268
|
+
fn validates_the_shared_wojjdg02_golden_vector() {
|
|
1269
|
+
let bytes = decode_hex(GOLDEN_HEX);
|
|
1270
|
+
let validated = validate_judge_package_with_options(
|
|
1271
|
+
&bytes,
|
|
1272
|
+
JudgePackageValidationOptions {
|
|
1273
|
+
expected_bytes: Some(863),
|
|
1274
|
+
expected_sha256: Some(GOLDEN_DIGEST),
|
|
1275
|
+
memory_limit_bytes: None,
|
|
1276
|
+
},
|
|
1277
|
+
)
|
|
1278
|
+
.unwrap();
|
|
1279
|
+
assert_eq!(validated.execution_semantic_sha256, GOLDEN_DIGEST);
|
|
1280
|
+
assert_eq!(validated.judge_data_case_count, 1);
|
|
1281
|
+
assert_eq!(validated.manifest.judge_kind, "text");
|
|
1282
|
+
assert_eq!(validated.manifest.allowed_languages, ["c"]);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
#[test]
|
|
1286
|
+
fn rejects_every_truncated_golden_prefix() {
|
|
1287
|
+
let bytes = decode_hex(GOLDEN_HEX);
|
|
1288
|
+
for length in 0..bytes.len() {
|
|
1289
|
+
assert!(
|
|
1290
|
+
validate_judge_package(&bytes[..length]).is_err(),
|
|
1291
|
+
"accepted truncated prefix of {length} bytes"
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
#[test]
|
|
1297
|
+
fn rejects_the_retired_transport_magic() {
|
|
1298
|
+
let mut bytes = decode_hex(GOLDEN_HEX);
|
|
1299
|
+
let retired_magic: Vec<u8> = [b"FORG".as_slice(), b"JDG1".as_slice()].concat();
|
|
1300
|
+
bytes[..8].copy_from_slice(&retired_magic);
|
|
1301
|
+
assert!(
|
|
1302
|
+
validate_judge_package(&bytes)
|
|
1303
|
+
.unwrap_err()
|
|
1304
|
+
.to_string()
|
|
1305
|
+
.contains("transport magic")
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
#[test]
|
|
1310
|
+
fn rejects_noncanonical_manifest_duplicate_blob_and_trailing_bytes() {
|
|
1311
|
+
let bytes = decode_hex(GOLDEN_HEX);
|
|
1312
|
+
|
|
1313
|
+
let mut noncanonical = bytes.clone();
|
|
1314
|
+
let manifest_length = u32::from_be_bytes(noncanonical[8..12].try_into().unwrap()) as usize;
|
|
1315
|
+
noncanonical[HEADER_BYTES + manifest_length - 1] = b' ';
|
|
1316
|
+
assert!(
|
|
1317
|
+
validate_judge_package(&noncanonical)
|
|
1318
|
+
.unwrap_err()
|
|
1319
|
+
.to_string()
|
|
1320
|
+
.contains("canonical JSON")
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
let blob_start = HEADER_BYTES + manifest_length;
|
|
1324
|
+
let mut duplicate = bytes.clone();
|
|
1325
|
+
duplicate[12..16].copy_from_slice(&2u32.to_be_bytes());
|
|
1326
|
+
duplicate.extend_from_within(blob_start..);
|
|
1327
|
+
assert!(
|
|
1328
|
+
validate_judge_package(&duplicate)
|
|
1329
|
+
.unwrap_err()
|
|
1330
|
+
.to_string()
|
|
1331
|
+
.contains("blob count disagrees")
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
let mut trailing = bytes.clone();
|
|
1335
|
+
trailing.push(0);
|
|
1336
|
+
assert!(
|
|
1337
|
+
validate_judge_package(&trailing)
|
|
1338
|
+
.unwrap_err()
|
|
1339
|
+
.to_string()
|
|
1340
|
+
.contains("trailing bytes")
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
#[test]
|
|
1345
|
+
fn rejects_blob_header_and_content_digest_corruption() {
|
|
1346
|
+
let bytes = decode_hex(GOLDEN_HEX);
|
|
1347
|
+
let manifest_length = u32::from_be_bytes(bytes[8..12].try_into().unwrap()) as usize;
|
|
1348
|
+
let blob_start = HEADER_BYTES + manifest_length;
|
|
1349
|
+
|
|
1350
|
+
let mut header = bytes.clone();
|
|
1351
|
+
header[blob_start] ^= 1;
|
|
1352
|
+
assert!(
|
|
1353
|
+
validate_judge_package(&header)
|
|
1354
|
+
.unwrap_err()
|
|
1355
|
+
.to_string()
|
|
1356
|
+
.contains("header disagrees")
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
let mut content = bytes;
|
|
1360
|
+
content[blob_start + BLOB_HEADER_BYTES] ^= 1;
|
|
1361
|
+
assert!(
|
|
1362
|
+
validate_judge_package(&content)
|
|
1363
|
+
.unwrap_err()
|
|
1364
|
+
.to_string()
|
|
1365
|
+
.contains("integrity verification")
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
fn u32_leb(mut value: u32) -> Vec<u8> {
|
|
1370
|
+
let mut bytes = Vec::new();
|
|
1371
|
+
loop {
|
|
1372
|
+
let mut byte = (value & 0x7f) as u8;
|
|
1373
|
+
value >>= 7;
|
|
1374
|
+
if value != 0 {
|
|
1375
|
+
byte |= 0x80;
|
|
1376
|
+
}
|
|
1377
|
+
bytes.push(byte);
|
|
1378
|
+
if value == 0 {
|
|
1379
|
+
return bytes;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
fn wasm_name(value: &str) -> Vec<u8> {
|
|
1385
|
+
let mut result = u32_leb(value.len() as u32);
|
|
1386
|
+
result.extend_from_slice(value.as_bytes());
|
|
1387
|
+
result
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
fn wasm_section(id: u8, payload: &[u8]) -> Vec<u8> {
|
|
1391
|
+
let mut result = vec![id];
|
|
1392
|
+
result.extend(u32_leb(payload.len() as u32));
|
|
1393
|
+
result.extend_from_slice(payload);
|
|
1394
|
+
result
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
fn command_module(
|
|
1398
|
+
import: Option<(&str, &str)>,
|
|
1399
|
+
start_section: bool,
|
|
1400
|
+
memory_flags: u8,
|
|
1401
|
+
) -> Vec<u8> {
|
|
1402
|
+
let imported = usize::from(import.is_some());
|
|
1403
|
+
let mut module = b"\0asm\x01\0\0\0".to_vec();
|
|
1404
|
+
let type_payload = if import.is_some() {
|
|
1405
|
+
vec![2, 0x60, 1, 0x7f, 0, 0x60, 0, 0]
|
|
1406
|
+
} else {
|
|
1407
|
+
vec![1, 0x60, 0, 0]
|
|
1408
|
+
};
|
|
1409
|
+
module.extend(wasm_section(1, &type_payload));
|
|
1410
|
+
if let Some((namespace, name)) = import {
|
|
1411
|
+
let mut payload = vec![1];
|
|
1412
|
+
payload.extend(wasm_name(namespace));
|
|
1413
|
+
payload.extend(wasm_name(name));
|
|
1414
|
+
payload.extend([0, 0]);
|
|
1415
|
+
module.extend(wasm_section(2, &payload));
|
|
1416
|
+
}
|
|
1417
|
+
module.extend(wasm_section(3, &[1, u8::from(import.is_some())]));
|
|
1418
|
+
let memory_payload = if memory_flags == 0 {
|
|
1419
|
+
vec![1, 0, 1]
|
|
1420
|
+
} else {
|
|
1421
|
+
vec![1, memory_flags, 1, 1]
|
|
1422
|
+
};
|
|
1423
|
+
module.extend(wasm_section(5, &memory_payload));
|
|
1424
|
+
let mut exports = vec![2];
|
|
1425
|
+
exports.extend(wasm_name("memory"));
|
|
1426
|
+
exports.extend([2, 0]);
|
|
1427
|
+
exports.extend(wasm_name("_start"));
|
|
1428
|
+
exports.extend([0, imported as u8]);
|
|
1429
|
+
module.extend(wasm_section(7, &exports));
|
|
1430
|
+
if start_section {
|
|
1431
|
+
module.extend(wasm_section(8, &[imported as u8]));
|
|
1432
|
+
}
|
|
1433
|
+
module.extend(wasm_section(10, &[1, 2, 0, 0x0b]));
|
|
1434
|
+
module
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
fn checker_package() -> (Vec<u8>, Vec<Vec<u8>>) {
|
|
1438
|
+
let golden = decode_hex(GOLDEN_HEX);
|
|
1439
|
+
let golden_manifest_length = u32::from_be_bytes(golden[8..12].try_into().unwrap()) as usize;
|
|
1440
|
+
let golden_blob_start = HEADER_BYTES + golden_manifest_length;
|
|
1441
|
+
let judge_data_length = u64::from_be_bytes(
|
|
1442
|
+
golden[golden_blob_start + 32..golden_blob_start + BLOB_HEADER_BYTES]
|
|
1443
|
+
.try_into()
|
|
1444
|
+
.unwrap(),
|
|
1445
|
+
) as usize;
|
|
1446
|
+
let judge_data = golden[golden_blob_start + BLOB_HEADER_BYTES
|
|
1447
|
+
..golden_blob_start + BLOB_HEADER_BYTES + judge_data_length]
|
|
1448
|
+
.to_vec();
|
|
1449
|
+
let artifact = command_module(None, false, 0);
|
|
1450
|
+
let asset = vec![0, 1, 2, 255];
|
|
1451
|
+
let judge_data_digest = sha256(&judge_data);
|
|
1452
|
+
let artifact_digest = sha256(&artifact);
|
|
1453
|
+
let asset_digest = sha256(&asset);
|
|
1454
|
+
let manifest = serde_json::json!({
|
|
1455
|
+
"allowedProfiles": {"c": {"optimization": "release", "target": "wasip1"}},
|
|
1456
|
+
"wasmOjContract": 2,
|
|
1457
|
+
"judge": {
|
|
1458
|
+
"args": [],
|
|
1459
|
+
"artifact": {"bytes": artifact.len(), "sha256": hex_digest(&artifact_digest)},
|
|
1460
|
+
"assets": [{
|
|
1461
|
+
"bytes": asset.len(),
|
|
1462
|
+
"guestPath": "/checker/assets/policy.bin",
|
|
1463
|
+
"sha256": hex_digest(&asset_digest),
|
|
1464
|
+
}],
|
|
1465
|
+
"kind": "checker",
|
|
1466
|
+
"runtimeProfile": "c-wasip1-release",
|
|
1467
|
+
},
|
|
1468
|
+
"judgeData": {"bytes": judge_data.len(), "sha256": hex_digest(&judge_data_digest)},
|
|
1469
|
+
"schema": "wasm-oj-v2/judge-package",
|
|
1470
|
+
});
|
|
1471
|
+
let mut manifest_bytes = serde_json::to_vec(&manifest).unwrap();
|
|
1472
|
+
manifest_bytes.push(b'\n');
|
|
1473
|
+
let mut remaining = vec![(artifact_digest, artifact), (asset_digest, asset)];
|
|
1474
|
+
remaining.sort_by_key(|(digest, _)| *digest);
|
|
1475
|
+
let ordered: Vec<_> = std::iter::once((judge_data_digest, judge_data))
|
|
1476
|
+
.chain(remaining)
|
|
1477
|
+
.collect();
|
|
1478
|
+
let mut package = Vec::new();
|
|
1479
|
+
package.extend_from_slice(WASM_OJ_JUDGE_PACKAGE_MAGIC);
|
|
1480
|
+
package.extend_from_slice(&(manifest_bytes.len() as u32).to_be_bytes());
|
|
1481
|
+
package.extend_from_slice(&(ordered.len() as u32).to_be_bytes());
|
|
1482
|
+
package.extend_from_slice(&manifest_bytes);
|
|
1483
|
+
let mut records = Vec::new();
|
|
1484
|
+
for (digest, contents) in ordered {
|
|
1485
|
+
let mut record = Vec::new();
|
|
1486
|
+
record.extend_from_slice(&digest);
|
|
1487
|
+
record.extend_from_slice(&(contents.len() as u64).to_be_bytes());
|
|
1488
|
+
record.extend_from_slice(&contents);
|
|
1489
|
+
package.extend_from_slice(&record);
|
|
1490
|
+
records.push(record);
|
|
1491
|
+
}
|
|
1492
|
+
(package, records)
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
#[test]
|
|
1496
|
+
fn validates_checker_artifact_integration_and_rejects_out_of_order_blobs() {
|
|
1497
|
+
let (package, records) = checker_package();
|
|
1498
|
+
let validated = validate_judge_package(&package).unwrap();
|
|
1499
|
+
assert_eq!(validated.manifest.judge_kind, "checker");
|
|
1500
|
+
|
|
1501
|
+
let manifest_length = u32::from_be_bytes(package[8..12].try_into().unwrap()) as usize;
|
|
1502
|
+
let records_start = HEADER_BYTES + manifest_length;
|
|
1503
|
+
let mut reordered = package[..records_start].to_vec();
|
|
1504
|
+
reordered.extend_from_slice(&records[0]);
|
|
1505
|
+
reordered.extend_from_slice(&records[2]);
|
|
1506
|
+
reordered.extend_from_slice(&records[1]);
|
|
1507
|
+
assert!(
|
|
1508
|
+
validate_judge_package(&reordered)
|
|
1509
|
+
.unwrap_err()
|
|
1510
|
+
.to_string()
|
|
1511
|
+
.contains("header disagrees")
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
#[test]
|
|
1516
|
+
fn statically_validates_trusted_wasm_and_rejects_malicious_capabilities() {
|
|
1517
|
+
let valid = command_module(None, false, 0);
|
|
1518
|
+
assert_eq!(
|
|
1519
|
+
validate_trusted_judge_wasm(&valid, Some(WASM_PAGE_BYTES))
|
|
1520
|
+
.unwrap()
|
|
1521
|
+
.initial_memory_pages,
|
|
1522
|
+
1
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1525
|
+
let network = command_module(Some(("wasi_snapshot_preview1", "sock_accept")), false, 0);
|
|
1526
|
+
assert!(validate_trusted_judge_wasm(&network, None).is_err());
|
|
1527
|
+
|
|
1528
|
+
let wasix = command_module(Some(("wasix_32v1", "fd_read")), false, 0);
|
|
1529
|
+
assert!(validate_trusted_judge_wasm(&wasix, None).is_err());
|
|
1530
|
+
|
|
1531
|
+
let start = command_module(None, true, 0);
|
|
1532
|
+
assert!(validate_trusted_judge_wasm(&start, None).is_err());
|
|
1533
|
+
|
|
1534
|
+
let shared = command_module(None, false, 3);
|
|
1535
|
+
assert!(validate_trusted_judge_wasm(&shared, None).is_err());
|
|
1536
|
+
|
|
1537
|
+
assert!(validate_trusted_judge_wasm(&valid[..valid.len() - 1], None).is_err());
|
|
1538
|
+
}
|
|
1539
|
+
}
|