containerd_shim_wasm/sandbox/
context.rs1use 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
12pub trait RuntimeContext: Send + Sync {
15 fn args(&self) -> &[String];
18
19 fn envs(&self) -> &[String];
21
22 fn entrypoint(&self) -> Entrypoint;
35}
36
37#[derive(Debug)]
39pub enum Source<'a> {
40 File(PathBuf),
42 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 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
78pub struct Entrypoint<'a> {
80 pub func: String,
82 pub name: Option<String>,
84 pub arg0: Option<&'a Path>,
86 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
141pub enum WasmBinaryType {
143 Module,
145 Component,
147}
148
149impl WasmBinaryType {
150 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}