wasmer_deploy_util/validation.rs
1/// Errors that can occur during module validation.
2#[derive(Clone, Debug)]
3pub enum ModuleValidationError {
4 WebProxyMissingSyscalls { syscalls: Vec<String> },
5 WcgiMissingSyscalls { syscalls: Vec<String> },
6}
7
8impl ModuleValidationError {
9 pub fn explain(&self) -> String {
10 match self {
11 Self::WebProxyMissingSyscalls { syscalls } => {
12 format!(
13 "web_proxy modules must read and write to sockets, \
14 but the module does not import the WASI functions required \
15 to do so. \\n
16 Missing functions: {}",
17 syscalls.join(", ")
18 )
19 }
20 Self::WcgiMissingSyscalls { syscalls } => {
21 format!(
22 "wcgi modules must read from stdin and write to stdout, \
23 but the module does not import the WASI functions required \
24 to do so. \n\
25 Missing functions: {}",
26 syscalls.join(", ")
27 )
28 }
29 }
30 }
31}
32
33impl std::fmt::Display for ModuleValidationError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::WebProxyMissingSyscalls { syscalls } => {
37 write!(
38 f,
39 "Module does not import required syscalls: {}",
40 syscalls.join(", ")
41 )
42 }
43 Self::WcgiMissingSyscalls { syscalls } => {
44 write!(
45 f,
46 "Module does not import required syscalls: {}",
47 syscalls.join(", ")
48 )
49 }
50 }
51 }
52}
53
54impl std::error::Error for ModuleValidationError {}
55
56#[derive(Clone, Debug)]
57pub enum CapabilityValidationError {
58 NetworkCapabilityMissing,
59}
60
61impl CapabilityValidationError {
62 pub fn explain(&self) -> String {
63 match self {
64 Self::NetworkCapabilityMissing => {
65 "vpn_proxy requires that the deployment configuration has
66 a network capability."
67 .to_string()
68 }
69 }
70 }
71}
72
73impl std::fmt::Display for CapabilityValidationError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::NetworkCapabilityMissing => {
77 write!(f, "Configuration does not have a network capability",)
78 }
79 }
80 }
81}
82
83impl std::error::Error for CapabilityValidationError {}
84
85/// Validate a WASM module for usage as a webproxy.
86///
87/// This only checks for the presence of some mandatory syscalls.
88#[allow(clippy::manual_flatten)]
89pub fn validate_parse_module_webproxy(bytes: &[u8]) -> Result<(), ModuleValidationError> {
90 let mut sock_listen = false;
91 let mut sock_accept = false;
92 let mut sock_recv = false;
93 let mut sock_send = false;
94
95 for payload in wasmparser::Parser::new(0).parse_all(bytes) {
96 // NOTE: validation deliberately ignores parse errors to be more compatible.
97 if let Ok(wasmparser::Payload::ImportSection(imports)) = payload {
98 for import in imports {
99 if let Ok(import) = import {
100 if !matches!(import.ty, wasmparser::TypeRef::Func(_)) {
101 continue;
102 }
103
104 // Checking is deliberately permissive, just check that the
105 // absolutely necessary syscalls are used.
106 if import.name.contains("sock_listen") {
107 sock_listen = true;
108 } else if import.name.contains("sock_accept") {
109 sock_accept = true;
110 } else if import.name.contains("sock_recv") {
111 sock_recv = true;
112 } else if import.name.contains("sock_send") {
113 sock_send = true;
114 }
115 }
116 }
117 }
118 }
119
120 let mut missing = Vec::new();
121 if !sock_listen {
122 // NOTE: not checking this, because WASI (non-wasix) would also work
123 // without listen, if there was a pre-defined socket.
124 // missing.push("sock_listen".to_string());
125 }
126 if !sock_accept {
127 missing.push("sock_accept".to_string());
128 }
129 if !sock_recv {
130 missing.push("sock_recv".to_string());
131 }
132 if !sock_send {
133 missing.push("sock_send".to_string());
134 }
135
136 if missing.is_empty() {
137 Ok(())
138 } else {
139 Err(ModuleValidationError::WebProxyMissingSyscalls { syscalls: missing })
140 }
141}
142
143/// Validate a WASM module for usage with wcgi .
144///
145/// This only checks for the presence of some mandatory syscalls.
146#[allow(clippy::manual_flatten)]
147pub fn validate_parse_module_wcgi(bytes: &[u8]) -> Result<(), ModuleValidationError> {
148 let mut fd_read = false;
149 let mut fd_write = false;
150 let mut fd_pipe = false;
151
152 for payload in wasmparser::Parser::new(0).parse_all(bytes) {
153 // NOTE: validation deliberately ignores parse errors to be more compatible.
154 if let Ok(wasmparser::Payload::ImportSection(imports)) = payload {
155 for import in imports {
156 if let Ok(import) = import {
157 if !matches!(import.ty, wasmparser::TypeRef::Func(_)) {
158 continue;
159 }
160
161 // Checking is deliberately permissive, just check that the
162 // absolutely necessary syscalls are used.
163 if import.name.contains("fd_read") {
164 fd_read = true;
165 } else if import.name.contains("fd_write") {
166 fd_write = true;
167 } else if import.name.contains("fd_pipe") {
168 fd_pipe = true;
169 }
170 }
171 }
172 }
173 }
174
175 let mut missing = Vec::new();
176 if !fd_read {
177 missing.push("fd_read".to_string());
178 }
179 if !(fd_write || fd_pipe) {
180 missing.push("fd_write|fd_pipe".to_string());
181 }
182
183 if missing.is_empty() {
184 Ok(())
185 } else {
186 Err(ModuleValidationError::WcgiMissingSyscalls { syscalls: missing })
187 }
188}
189
190#[derive(Clone, Debug)]
191pub enum DeploymentValidationError {
192 Module(ModuleValidationError),
193 MissingCapability(CapabilityValidationError),
194 UnsupportedRunner(String),
195}
196
197impl std::fmt::Display for DeploymentValidationError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 Self::Module(e) => e.fmt(f),
201 Self::MissingCapability(e) => e.fmt(f),
202 Self::UnsupportedRunner(e) => write!(f, "Unsupported runner: {}", e),
203 }
204 }
205}
206
207// /// Validate a module against the given deployment config.
208// ///
209// /// Will check that the module is compatible with the runner.
210// pub fn validate_deployment_module(
211// depl: &DeploymentV1,
212// module_wasm: &[u8],
213// ) -> Result<(), DeploymentValidationError> {
214// match &depl.workload.runner {
215// WorkloadRunnerV1::Wasm(_) => Err(DeploymentValidationError::UnsupportedRunner(
216// "wasm".to_string(),
217// )),
218// WorkloadRunnerV1::WebcCommand(_) => Err(DeploymentValidationError::UnsupportedRunner(
219// "webc_command".to_string(),
220// )),
221// WorkloadRunnerV1::TcpProxy(_) => Err(DeploymentValidationError::UnsupportedRunner(
222// "tcp_proxy".to_string(),
223// )),
224// WorkloadRunnerV1::WCgi(_) => {
225// validate_parse_module_wcgi(module_wasm).map_err(DeploymentValidationError::Module)
226// }
227// WorkloadRunnerV1::WebProxy(_) => {
228// validate_parse_module_webproxy(module_wasm).map_err(DeploymentValidationError::Module)
229// }
230// }
231// }
232
233#[cfg(test)]
234mod tests {
235 use std::path::PathBuf;
236
237 // use wasmer_deploy_schema::schema::{
238 // RunnerWCgiV1, WorkloadRunnerWasmSourceLocalPathV1, WorkloadRunnerWasmSourceV1,
239 // };
240
241 use super::*;
242
243 fn root_path() -> PathBuf {
244 std::env::var("CARGO_MANIFEST_DIR")
245 .map(PathBuf::from)
246 .unwrap()
247 .parent()
248 .unwrap()
249 .parent()
250 .unwrap()
251 .to_owned()
252 }
253
254 fn tests_path() -> PathBuf {
255 root_path().join("wasm-tests").join("compiled")
256 }
257
258 #[test]
259 fn test_validate_wcgi_valid() {
260 let path = tests_path().join("local").join("wcgi-hello.wasm");
261 let contents = std::fs::read(path).unwrap();
262 validate_parse_module_wcgi(&contents).unwrap();
263 }
264
265 #[test]
266 fn test_validate_wcgi_invalid() {
267 let path = tests_path().join("vendor").join("empty.wasm");
268 let contents = std::fs::read(path).unwrap();
269
270 let res = validate_parse_module_wcgi(&contents);
271 assert!(matches!(
272 res,
273 Err(ModuleValidationError::WcgiMissingSyscalls { .. })
274 ));
275 }
276
277 #[test]
278 fn test_validate_webproxy_valid() {
279 let path = tests_path().join("vendor").join("static-web-server.wasm");
280 let contents = std::fs::read(path).unwrap();
281
282 validate_parse_module_webproxy(&contents).unwrap();
283 }
284
285 #[test]
286 fn test_validate_webproxy_invalid() {
287 let path = tests_path().join("local").join("wcgi-hello.wasm");
288 let contents = std::fs::read(path).unwrap();
289
290 let res = validate_parse_module_webproxy(&contents);
291 assert!(matches!(
292 res,
293 Err(ModuleValidationError::WebProxyMissingSyscalls { .. })
294 ));
295 }
296
297 // #[test]
298 // fn test_validate_deployment_wcgi() {
299 // // Valid
300
301 // let path = tests_path().join("local").join("wcgi-hello.wasm");
302 // let contents = std::fs::read(path).unwrap();
303
304 // let depl = DeploymentV1 {
305 // name: "name".to_string(),
306 // workload: wasmer_deploy_schema::schema::WorkloadV1 {
307 // name: None,
308 // capabilities: Default::default(),
309 // runner: WorkloadRunnerV1::WCgi(RunnerWCgiV1 {
310 // source: WorkloadRunnerWasmSourceV1::LocalPath(
311 // WorkloadRunnerWasmSourceLocalPathV1 {
312 // path: ".".to_string(),
313 // },
314 // ),
315 // dialect: None,
316 // }),
317 // },
318 // };
319
320 // validate_deployment_module(&depl, &contents).unwrap();
321
322 // // Invalid.
323
324 // let path = tests_path().join("vendor").join("empty.wasm");
325 // let contents = std::fs::read(path).unwrap();
326
327 // assert!(matches!(
328 // validate_deployment_module(&depl, &contents),
329 // Err(DeploymentValidationError::Module(
330 // ModuleValidationError::WcgiMissingSyscalls { .. }
331 // ))
332 // ));
333 // }
334
335 // #[test]
336 // fn test_validate_deployment_web_proxy() {
337 // // Valid
338
339 // let path = tests_path().join("vendor").join("static-web-server.wasm");
340 // let contents = std::fs::read(path).unwrap();
341
342 // let depl = DeploymentV1 {
343 // name: "name".to_string(),
344 // workload: wasmer_deploy_schema::schema::WorkloadV1 {
345 // name: None,
346 // capabilities: Default::default(),
347 // runner: WorkloadRunnerV1::WebProxy(
348 // wasmer_deploy_schema::schema::RunnerWebProxyV1 {
349 // source: WorkloadRunnerWasmSourceV1::LocalPath(
350 // WorkloadRunnerWasmSourceLocalPathV1 {
351 // path: ".".to_string(),
352 // },
353 // ),
354 // },
355 // ),
356 // },
357 // };
358
359 // validate_deployment_module(&depl, &contents).unwrap();
360
361 // // Invalid.
362
363 // let path = tests_path().join("local").join("wcgi-hello.wasm");
364 // let contents = std::fs::read(path).unwrap();
365
366 // assert!(matches!(
367 // validate_deployment_module(&depl, &contents),
368 // Err(DeploymentValidationError::Module(
369 // ModuleValidationError::WebProxyMissingSyscalls { .. }
370 // ))
371 // ));
372 // }
373}