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}