containerd_shim_wasm/sandbox/
context.rs

1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, bail};
5use oci_spec::image::Descriptor;
6use oci_spec::runtime::Spec;
7use serde::{Deserialize, Serialize};
8use wasmparser::Parser;
9
10use crate::sandbox::path::PathResolve;
11
12/// The `RuntimeContext` trait provides access to the runtime context that includes
13/// the arguments, environment variables, and entrypoint for the container.
14pub trait RuntimeContext: Send + Sync {
15    /// Returns arguments from the runtime spec process field, including the
16    /// path to the entrypoint executable.
17    fn args(&self) -> &[String];
18
19    /// Returns environment variables in the format `ENV_VAR_NAME=VALUE` from the runtime spec process field.
20    fn envs(&self) -> &[String];
21
22    /// Returns a `Entrypoint` with the following fields obtained from the first argument in the OCI spec for entrypoint:
23    ///   - `arg0` - raw entrypoint from the OCI spec
24    ///   - `name` - provided as the file name of the module in the entrypoint without the extension
25    ///   - `func` - name of the exported function to call, obtained from the arguments on process OCI spec.
26    ///   - `Source` - either a `File(PathBuf)` or `Oci(WasmLayer)`. When a `File` source the `PathBuf`` is provided by entrypoint in OCI spec.
27    ///     If the image contains custom OCI Wasm layers, the source is provided as an array of `WasmLayer` structs.
28    ///
29    /// The first argument in the OCI spec for entrypoint is specified as `path#func` where `func` is optional
30    /// and defaults to _start, e.g.:
31    ///   "/app/app.wasm#entry" -> { source: File("/app/app.wasm"), func: "entry", name: "Some(app)", arg0: "/app/app.wasm#entry" }
32    ///   "my_module.wat" -> { source: File("my_module.wat"), func: "_start", name: "Some(my_module)", arg0: "my_module.wat" }
33    ///   "#init" -> { source: File(""), func: "init", name: None, arg0: "#init" }
34    fn entrypoint(&self) -> Entrypoint;
35}
36
37/// The source for a WASI module / components.
38#[derive(Debug)]
39pub enum Source<'a> {
40    /// The WASI module is a file in the file system.
41    File(PathBuf),
42    /// The WASI module / component is provided as a layer in the OCI spec.
43    /// For a WASI preview 1 module this is usually a single element array.
44    /// For a WASI preview 2 component this is an array of one or more
45    /// elements, where each element is a component.
46    /// Runtimes can additionally provide a list of layer types they support,
47    /// and they will be included in this array, e.g., a `toml` file with the
48    /// runtime configuration.
49    Oci(&'a [WasmLayer]),
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct WasmLayer {
54    pub config: Descriptor,
55    #[serde(with = "serde_bytes")]
56    pub layer: Vec<u8>,
57}
58
59impl<'a> Source<'a> {
60    /// Returns the bytes of the WASI module / component.
61    pub fn as_bytes(&self) -> anyhow::Result<Cow<'a, [u8]>> {
62        match self {
63            Source::File(path) => {
64                let path = path
65                    .resolve_in_path_or_cwd()
66                    .next()
67                    .context("module not found")?;
68                Ok(Cow::Owned(std::fs::read(path)?))
69            }
70            Source::Oci([module]) => Ok(Cow::Borrowed(&module.layer)),
71            Source::Oci(_modules) => {
72                bail!("only a single module is supported when using images with OCI layers")
73            }
74        }
75    }
76}
77
78/// The entrypoint for a WASI module / component.
79pub struct Entrypoint<'a> {
80    /// The name of the exported function to call. Defaults to "_start".
81    pub func: String,
82    /// The name of the WASI module / component without the extension.
83    pub name: Option<String>,
84    /// The first argument in the OCI spec for entrypoint.
85    pub arg0: Option<&'a Path>,
86    /// The source of the WASI module / component, either a file or an OCI layer.
87    pub source: Source<'a>,
88}
89
90pub(crate) struct WasiContext<'a> {
91    pub spec: &'a Spec,
92    pub wasm_layers: &'a [WasmLayer],
93}
94
95impl RuntimeContext for WasiContext<'_> {
96    fn args(&self) -> &[String] {
97        self.spec
98            .process()
99            .as_ref()
100            .and_then(|p| p.args().as_ref())
101            .map(|a| a.as_slice())
102            .unwrap_or_default()
103    }
104
105    fn envs(&self) -> &[String] {
106        self.spec
107            .process()
108            .as_ref()
109            .and_then(|p| p.env().as_ref())
110            .map(|a| a.as_slice())
111            .unwrap_or_default()
112    }
113
114    fn entrypoint(&self) -> Entrypoint {
115        let arg0 = self.args().first();
116
117        let entry_point = arg0.map(String::as_str).unwrap_or("");
118        let (path, func) = entry_point
119            .split_once('#')
120            .unwrap_or((entry_point, "_start"));
121
122        let source = if self.wasm_layers.is_empty() {
123            Source::File(PathBuf::from(path))
124        } else {
125            Source::Oci(self.wasm_layers)
126        };
127
128        let module_name = PathBuf::from(path)
129            .file_stem()
130            .map(|name| name.to_string_lossy().to_string());
131
132        Entrypoint {
133            func: func.to_string(),
134            arg0: arg0.map(Path::new),
135            source,
136            name: module_name,
137        }
138    }
139}
140
141/// The type of a wasm binary.
142pub enum WasmBinaryType {
143    /// A wasm module.
144    Module,
145    /// A wasm component.
146    Component,
147}
148
149impl WasmBinaryType {
150    /// Returns the type of the wasm binary.
151    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
152        if Parser::is_component(bytes) {
153            Some(Self::Component)
154        } else if Parser::is_core_wasm(bytes) {
155            Some(Self::Module)
156        } else {
157            None
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use anyhow::Result;
165    use oci_spec::image::{Descriptor, Digest};
166    use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder};
167
168    use super::*;
169
170    #[test]
171    fn test_get_args() -> Result<()> {
172        let spec = SpecBuilder::default()
173            .root(RootBuilder::default().path("rootfs").build()?)
174            .process(
175                ProcessBuilder::default()
176                    .cwd("/")
177                    .args(vec!["hello.wat".to_string()])
178                    .build()?,
179            )
180            .build()?;
181
182        let ctx = WasiContext {
183            spec: &spec,
184            wasm_layers: &[],
185        };
186
187        let args = ctx.args();
188        assert_eq!(args.len(), 1);
189        assert_eq!(args[0], "hello.wat");
190
191        Ok(())
192    }
193
194    #[test]
195    fn test_get_args_return_empty() -> Result<()> {
196        let spec = SpecBuilder::default()
197            .root(RootBuilder::default().path("rootfs").build()?)
198            .process(ProcessBuilder::default().cwd("/").args(vec![]).build()?)
199            .build()?;
200
201        let ctx = WasiContext {
202            spec: &spec,
203            wasm_layers: &[],
204        };
205
206        let args = ctx.args();
207        assert_eq!(args.len(), 0);
208
209        Ok(())
210    }
211
212    #[test]
213    fn test_get_args_returns_all() -> Result<()> {
214        let spec = SpecBuilder::default()
215            .root(RootBuilder::default().path("rootfs").build()?)
216            .process(
217                ProcessBuilder::default()
218                    .cwd("/")
219                    .args(vec![
220                        "hello.wat".to_string(),
221                        "echo".to_string(),
222                        "hello".to_string(),
223                    ])
224                    .build()?,
225            )
226            .build()?;
227
228        let ctx = WasiContext {
229            spec: &spec,
230            wasm_layers: &[],
231        };
232
233        let args = ctx.args();
234        assert_eq!(args.len(), 3);
235        assert_eq!(args[0], "hello.wat");
236        assert_eq!(args[1], "echo");
237        assert_eq!(args[2], "hello");
238
239        Ok(())
240    }
241
242    #[test]
243    fn test_get_module_returns_none_when_not_present() -> Result<()> {
244        let spec = SpecBuilder::default()
245            .root(RootBuilder::default().path("rootfs").build()?)
246            .process(ProcessBuilder::default().cwd("/").args(vec![]).build()?)
247            .build()?;
248
249        let ctx = WasiContext {
250            spec: &spec,
251            wasm_layers: &[],
252        };
253
254        let path = ctx.entrypoint().source;
255        assert!(matches!(
256            path,
257            Source::File(p) if p.as_os_str().is_empty()
258        ));
259
260        Ok(())
261    }
262
263    #[test]
264    fn test_get_module_returns_function() -> Result<()> {
265        let spec = SpecBuilder::default()
266            .root(RootBuilder::default().path("rootfs").build()?)
267            .process(
268                ProcessBuilder::default()
269                    .cwd("/")
270                    .args(vec![
271                        "hello.wat#foo".to_string(),
272                        "echo".to_string(),
273                        "hello".to_string(),
274                    ])
275                    .build()?,
276            )
277            .build()?;
278
279        let ctx = WasiContext {
280            spec: &spec,
281            wasm_layers: &[],
282        };
283
284        let expected_path = PathBuf::from("hello.wat");
285        let Entrypoint {
286            name,
287            func,
288            arg0,
289            source,
290        } = ctx.entrypoint();
291        assert_eq!(name, Some("hello".to_string()));
292        assert_eq!(func, "foo");
293        assert_eq!(arg0, Some(Path::new("hello.wat#foo")));
294        assert!(matches!(
295            source,
296            Source::File(p) if p == expected_path
297        ));
298
299        Ok(())
300    }
301
302    #[test]
303    fn test_get_module_returns_start() -> Result<()> {
304        let spec = SpecBuilder::default()
305            .root(RootBuilder::default().path("rootfs").build()?)
306            .process(
307                ProcessBuilder::default()
308                    .cwd("/")
309                    .args(vec![
310                        "/root/hello.wat".to_string(),
311                        "echo".to_string(),
312                        "hello".to_string(),
313                    ])
314                    .build()?,
315            )
316            .build()?;
317
318        let ctx = WasiContext {
319            spec: &spec,
320            wasm_layers: &[],
321        };
322
323        let expected_path = PathBuf::from("/root/hello.wat");
324        let Entrypoint {
325            name,
326            func,
327            arg0,
328            source,
329        } = ctx.entrypoint();
330        assert_eq!(name, Some("hello".to_string()));
331        assert_eq!(func, "_start");
332        assert_eq!(arg0, Some(Path::new("/root/hello.wat")));
333        assert!(matches!(
334            source,
335            Source::File(p) if p == expected_path
336        ));
337
338        Ok(())
339    }
340
341    #[test]
342    fn test_loading_strategy_is_file_when_no_layers() -> Result<()> {
343        let spec = SpecBuilder::default()
344            .root(RootBuilder::default().path("rootfs").build()?)
345            .process(
346                ProcessBuilder::default()
347                    .cwd("/")
348                    .args(vec![
349                        "/root/hello.wat#foo".to_string(),
350                        "echo".to_string(),
351                        "hello".to_string(),
352                    ])
353                    .build()?,
354            )
355            .build()?;
356
357        let ctx = WasiContext {
358            spec: &spec,
359            wasm_layers: &[],
360        };
361
362        let expected_path = PathBuf::from("/root/hello.wat");
363        assert!(matches!(
364            ctx.entrypoint().source,
365            Source::File(p) if p == expected_path
366        ));
367
368        Ok(())
369    }
370
371    #[test]
372    fn test_loading_strategy_is_oci_when_layers_present() -> Result<()> {
373        let spec = SpecBuilder::default()
374            .root(RootBuilder::default().path("rootfs").build()?)
375            .process(
376                ProcessBuilder::default()
377                    .cwd("/")
378                    .args(vec![
379                        "/root/hello.wat".to_string(),
380                        "echo".to_string(),
381                        "hello".to_string(),
382                    ])
383                    .build()?,
384            )
385            .build()?;
386
387        let ctx = WasiContext {
388            spec: &spec,
389            wasm_layers: &[WasmLayer {
390                layer: vec![],
391                config: Descriptor::new(
392                    oci_spec::image::MediaType::Other("".to_string()),
393                    10,
394                    Digest::try_from(format!("sha256:{:064?}", 0))?,
395                ),
396            }],
397        };
398
399        assert!(matches!(ctx.entrypoint().source, Source::Oci(_)));
400
401        Ok(())
402    }
403
404    #[test]
405    fn test_get_envs() -> Result<()> {
406        let spec = SpecBuilder::default()
407            .root(RootBuilder::default().path("rootfs").build()?)
408            .process(
409                ProcessBuilder::default()
410                    .cwd("/")
411                    .env(vec!["KEY1=VALUE1".to_string(), "KEY2=VALUE2".to_string()])
412                    .build()?,
413            )
414            .build()?;
415
416        let ctx = WasiContext {
417            spec: &spec,
418            wasm_layers: &[],
419        };
420
421        let envs = ctx.envs();
422        assert_eq!(envs.len(), 2);
423        assert_eq!(envs[0], "KEY1=VALUE1");
424        assert_eq!(envs[1], "KEY2=VALUE2");
425
426        Ok(())
427    }
428
429    #[test]
430    fn test_get_envs_return_empty() -> Result<()> {
431        let spec = SpecBuilder::default()
432            .root(RootBuilder::default().path("rootfs").build()?)
433            .process(ProcessBuilder::default().cwd("/").env(vec![]).build()?)
434            .build()?;
435
436        let ctx = WasiContext {
437            spec: &spec,
438            wasm_layers: &[],
439        };
440
441        let envs = ctx.envs();
442        assert_eq!(envs.len(), 0);
443
444        Ok(())
445    }
446
447    #[test]
448    fn test_envs_return_default_only() -> Result<()> {
449        let spec = SpecBuilder::default()
450            .root(RootBuilder::default().path("rootfs").build()?)
451            .process(ProcessBuilder::default().cwd("/").build()?)
452            .build()?;
453
454        let ctx = WasiContext {
455            spec: &spec,
456            wasm_layers: &[],
457        };
458
459        let envs = ctx.envs();
460        assert_eq!(envs.len(), 2);
461
462        Ok(())
463    }
464}