From 5189873eb313e6bbfbd170996aa6609c9602c384 Mon Sep 17 00:00:00 2001 From: Ismo Puustinen Date: Tue, 9 May 2023 14:40:11 +0300 Subject: [PATCH] wasmedge: OCI spec tests. Add tests which use external Wasm modules to test the OCI spec features. Signed-off-by: Ismo Puustinen --- .../tests/oci_tests.rs | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 crates/containerd-shim-wasmedge/tests/oci_tests.rs diff --git a/crates/containerd-shim-wasmedge/tests/oci_tests.rs b/crates/containerd-shim-wasmedge/tests/oci_tests.rs new file mode 100644 index 000000000..08a0bf2bf --- /dev/null +++ b/crates/containerd-shim-wasmedge/tests/oci_tests.rs @@ -0,0 +1,286 @@ +use std::borrow::Cow; +use std::fs::{create_dir, read_to_string, File, OpenOptions}; +use std::io::prelude::*; +use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::channel; +use std::sync::Arc; +use std::time::Duration; + +use chrono::{DateTime, Utc}; +use libc::SIGKILL; +use serde::{Deserialize, Serialize}; +use serial_test::serial; +use tempfile::{tempdir, TempDir}; + +use oci_spec::runtime::{ + LinuxBuilder, LinuxSeccompAction, LinuxSeccompBuilder, LinuxSyscallBuilder, ProcessBuilder, + RootBuilder, Spec, SpecBuilder, +}; + +use containerd_shim_wasm::function; +use containerd_shim_wasm::sandbox::exec::has_cap_sys_admin; +use containerd_shim_wasm::sandbox::instance::Wait; +use containerd_shim_wasm::sandbox::testutil::run_test_with_sudo; +use containerd_shim_wasm::sandbox::{EngineGetter, Error, Instance, InstanceConfig}; +use containerd_shim_wasmedge::instance::{reset_stdio, Wasi}; + +#[derive(Serialize, Deserialize)] +struct Options { + root: Option, +} + +fn get_external_wasm_module(name: String) -> Result, Error> { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let target = Path::new(manifest_dir) + .join("../../target/wasm32-wasi/debug") + .join(name.clone()); + std::fs::read(target).map_err(|e| { + Error::Others(format!( + "failed to read requested Wasm module ({}): {}. Perhaps you need to run 'make test/wasm-modules' first.", + name, e + )) + }) +} + +fn run_wasi_test_with_spec( + dir: &TempDir, + spec: &Spec, + wasmbytes: Cow<[u8]>, +) -> Result<(u32, DateTime), Error> { + create_dir(dir.path().join("rootfs"))?; + let rootdir = dir.path().join("runwasi"); + create_dir(&rootdir)?; + let opts = Options { + root: Some(rootdir), + }; + let opts_file = OpenOptions::new() + .read(true) + .create(true) + .truncate(true) + .write(true) + .open(dir.path().join("options.json"))?; + write!(&opts_file, "{}", serde_json::to_string(&opts)?)?; + + let wasm_path = dir.path().join("rootfs/file.wasm"); + let mut f = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o755) + .open(wasm_path)?; + f.write_all(&wasmbytes)?; + + let stdout = File::create(dir.path().join("stdout"))?; + drop(stdout); + + spec.save(dir.path().join("config.json"))?; + + let mut cfg = InstanceConfig::new(Wasi::new_engine()?, "test_namespace".into()); + let cfg = cfg + .set_bundle(dir.path().to_str().unwrap().to_string()) + .set_stdout(dir.path().join("stdout").to_str().unwrap().to_string()); + + let wasi = Arc::new(Wasi::new("test".to_string(), Some(cfg))); + + wasi.start()?; + + let (tx, rx) = channel(); + let waiter = Wait::new(tx); + wasi.wait(&waiter).unwrap(); + + let res = match rx.recv_timeout(Duration::from_secs(10)) { + Ok(res) => Ok(res), + Err(e) => { + wasi.kill(SIGKILL as u32).unwrap(); + return Err(Error::Others(format!( + "error waiting for module to finish: {0}", + e + ))); + } + }; + wasi.delete()?; + res +} + +#[test] +#[serial] +fn test_external_hello_world() -> Result<(), Error> { + if !has_cap_sys_admin() { + println!("running test with sudo: {}", function!()); + return run_test_with_sudo("test_external_hello_world"); + } + + let dir = tempdir()?; + let path = dir.path(); + + let wasmbytes = get_external_wasm_module("hello-world.wasm".to_string())?; + + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["./file.wasm".to_string()]) + .build()?, + ) + .build()?; + + let res = run_wasi_test_with_spec(&dir, &spec, Cow::from(wasmbytes))?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert!(output.starts_with("hello world")); + + reset_stdio(); + Ok(()) +} + +#[test] +#[serial] +fn test_seccomp_hello_world_pass() -> Result<(), Error> { + if !has_cap_sys_admin() { + println!("running test with sudo: {}", function!()); + return run_test_with_sudo("test_seccomp_hello_world_pass"); + } + + let dir = tempdir()?; + let path = dir.path(); + + let wasmbytes = get_external_wasm_module("hello-world.wasm".to_string())?; + + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["./file.wasm".to_string()]) + .build()?, + ) + .linux( + LinuxBuilder::default() + .seccomp( + LinuxSeccompBuilder::default() + .default_action(LinuxSeccompAction::ScmpActAllow) + .architectures(vec![oci_spec::runtime::Arch::ScmpArchNative]) + .syscalls(vec![LinuxSyscallBuilder::default() + .names(vec!["getcwd".to_string()]) + .action(LinuxSeccompAction::ScmpActAllow) + .build()?]) + .build()?, + ) + .build()?, + ) + .build()?; + + let res = run_wasi_test_with_spec(&dir, &spec, Cow::from(wasmbytes))?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert!(output.starts_with("hello world")); + + reset_stdio(); + Ok(()) +} + +#[test] +#[serial] +fn test_seccomp_hello_world_fail() -> Result<(), Error> { + if !has_cap_sys_admin() { + println!("running test with sudo: {}", function!()); + return run_test_with_sudo("test_seccomp_hello_world_fail"); + } + + let dir = tempdir()?; + + let wasmbytes = get_external_wasm_module("hello-world.wasm".to_string())?; + + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["./file.wasm".to_string()]) + .build()?, + ) + .linux( + LinuxBuilder::default() + .seccomp( + LinuxSeccompBuilder::default() + .default_action(LinuxSeccompAction::ScmpActAllow) + .architectures(vec![oci_spec::runtime::Arch::ScmpArchNative]) + .syscalls(vec![LinuxSyscallBuilder::default() + .names(vec!["getcwd".to_string()]) // Do not allow getcwd() + .action(LinuxSeccompAction::ScmpActErrno) + .build()?]) + .build()?, + ) + .build()?, + ) + .build()?; + + let res = run_wasi_test_with_spec(&dir, &spec, Cow::from(wasmbytes))?; + + assert_ne!(res.0, 0); // Returns an error + + reset_stdio(); + Ok(()) +} + +#[test] +#[serial] +#[ignore] +fn test_seccomp_hello_world_notify() -> Result<(), Error> { + // Test how seccomp works together with an external notification agent. + // Configure the external agent to use socket /tmp/seccomp-agent.socket + // and set it to either allow or decline (with error) "writev" system + // call. + + if !has_cap_sys_admin() { + println!("running test with sudo: {}", function!()); + return run_test_with_sudo(function!()); + } + + let dir = tempdir()?; + let path = dir.path(); + + let wasmbytes = get_external_wasm_module("hello-world.wasm".to_string())?; + + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["./file.wasm".to_string()]) + .build()?, + ) + .linux( + LinuxBuilder::default() + .seccomp( + LinuxSeccompBuilder::default() + .default_action(LinuxSeccompAction::ScmpActAllow) + .architectures(vec![oci_spec::runtime::Arch::ScmpArchNative]) + .syscalls(vec![LinuxSyscallBuilder::default() + .names(vec!["getcwd".to_string()]) // system call 72 + .action(LinuxSeccompAction::ScmpActNotify) + .build()?]) + .listener_path("/tmp/seccomp-agent.socket") + .build()?, + ) + .build()?, + ) + .build()?; + + let res = run_wasi_test_with_spec(&dir, &spec, Cow::from(wasmbytes))?; + + assert_eq!(res.0, 0); // Returns success or error, depending on how the external agent is configured + + let output = read_to_string(path.join("stdout"))?; + assert!(output.starts_with("hello world")); + + reset_stdio(); + + Ok(()) +}