Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/containerd-shim-wasm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Fixed
- Linux (native) containers now run with the environment from their OCI spec. `Executor::setup_envs` was a no-op for every container type, so a native container sharing a pod with a Wasm container (for example Knative's `queue-proxy`) ran with the shim's own environment, including its `PATH`. ([#1061](https://github.com/containerd/runwasi/issues/1061))

## [v1.0.1] - 2025-06-10

## [v1.0.0]
Expand Down
167 changes: 162 additions & 5 deletions crates/containerd-shim-wasm/src/sys/unix/container/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,33 @@ impl<S: Shim> LibcontainerExecutor for Executor<S> {
}
}

// This is an no-op for the Wasm `Executor`. Instead of youki's libcontainer setting the envs
// in the shim process, the shim will manage the envs itself. The expectation is that the shim will
// call `RuntimeContext::envs()` to get the container's envs and set them in the `Engine::run_wasi`
// For Wasm containers this is a no-op: instead of youki's libcontainer setting the envs in the
// shim process, the shim manages the envs itself. The expectation is that the shim will call
// `RuntimeContext::envs()` to get the container's envs and set them in the `Engine::run_wasi`
// function. This way, the shim can decide how to pass the envs to the WASI context.
//
// Linux (native) containers have no such shim code, so for them we fall back to libcontainer's
// default behaviour (clear the host's envs, then set the ones from the OCI spec). Without this a
// native container sharing a pod with a Wasm container (e.g. Knative's queue-proxy) runs with
// the shim's own environment, including its PATH.
//
// libcontainer calls `validate` right before `setup_envs`, so the container type is known here.
//
// See the following issues for more context:
// https://github.com/containerd/runwasi/issues/619
// https://github.com/containerd/runwasi/issues/1061
// https://github.com/containers/youki/issues/2815
fn setup_envs(
&self,
_: HashMap<String, String>,
envs: HashMap<String, String>,
) -> std::result::Result<(), ExecutorSetEnvsError> {
Ok(())
match self.0.ty.get() {
Some(ExecutorType::Linux) => DefaultExecutor {}.setup_envs(envs),
Some(ExecutorType::Wasm(_) | ExecutorType::CantHandle) => Ok(()),
None => Err(ExecutorSetEnvsError::Other(
"setup_envs called before validate: unknown container type".to_string(),
)),
}
}
}

Expand Down Expand Up @@ -152,3 +166,146 @@ fn is_linux_container(ctx: &impl RuntimeContext) -> Result<()> {
_ => bail!("not a valid script or elf file"),
}
}

#[cfg(test)]
mod tests {
use std::process::Command;

use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder};

use super::*;
use crate::sandbox::Sandbox;
use crate::sandbox::context::RuntimeContext;

#[derive(Default)]
struct TestSandbox;

impl Sandbox for TestSandbox {
async fn can_handle(&self, _ctx: &impl RuntimeContext) -> Result<()> {
Ok(())
}
async fn run_wasi(&self, _ctx: &impl RuntimeContext) -> Result<i32> {
Ok(0)
}
}

struct TestShim;

impl Shim for TestShim {
fn name() -> &'static str {
"test"
}
type Sandbox = TestSandbox;
}

fn spec(args: &[&str], env: &[&str]) -> Spec {
SpecBuilder::default()
.root(RootBuilder::default().path("/").build().unwrap())
.process(
ProcessBuilder::default()
.cwd("/")
.args(args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
.env(env.iter().map(|s| s.to_string()).collect::<Vec<_>>())
.build()
.unwrap(),
)
.build()
.unwrap()
}

fn envs(spec: &Spec) -> HashMap<String, String> {
spec.process()
.as_ref()
.and_then(|p| p.env().as_ref())
.into_iter()
.flatten()
.filter_map(|e| e.split_once('='))
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}

const HELPER_ENV: &str = "RUNWASI_TEST_EXEC_HELPER";

/// Runs the same sequence libcontainer's init process runs (`validate`, `setup_envs`, `exec`)
/// in this process. It replaces the process on success, so it only does something as the
/// helper child of `run_like_libcontainer`; as a normal test it returns immediately.
#[test]
fn exec_helper() {
let Ok(program) = std::env::var(HELPER_ENV) else {
return;
};
let spec = spec(
&[program.as_str(), "env"],
&["FOO=bar", "CONTAINER_CONCURRENCY=0", "PATH=/container/bin"],
);
let executor = Executor::<TestShim>::new(vec![]);
executor.validate(&spec).unwrap();
executor.setup_envs(envs(&spec)).unwrap();
let err = executor.exec(&spec).unwrap_err();
panic!("exec returned: {err}");
}

/// Re-runs this test binary as a clean child (no fork in a multithreaded process) with a
/// polluted "shim" environment, and returns what the exec'd program printed.
fn run_like_libcontainer(program: &str) -> String {
let test_name = format!(
"{}::exec_helper",
module_path!().split_once("::").unwrap().1
);
let out = Command::new(std::env::current_exe().unwrap())
.args([
test_name.as_str(),
"--exact",
"--nocapture",
"--test-threads=1",
])
.env(HELPER_ENV, program)
.env("SHIM_ONLY_VAR", "leaked")
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).into_owned()
}

// Reproduces https://github.com/containerd/runwasi/issues/1061 with a static busybox:
// `env` must print the container's env and nothing from the shim's environment.
#[test]
fn linux_container_gets_its_own_env() {
let Some(busybox) = ["/usr/bin/busybox", "/bin/busybox"]
.into_iter()
.find(|p| std::path::Path::new(p).exists())
else {
eprintln!("busybox not installed, skipping");
return;
};
let out = run_like_libcontainer(busybox);
// (the first line shares a line with the test harness's "test ... " prefix)
assert!(out.contains("FOO=bar"), "{out}");
assert!(out.contains("CONTAINER_CONCURRENCY=0"), "{out}");
assert!(out.contains("PATH=/container/bin"), "{out}");
assert!(
!out.contains("SHIM_ONLY_VAR"),
"shim env leaked into container: {out}"
);
}

#[test]
fn setup_envs_before_validate_is_an_error() {
let executor = Executor::<TestShim>::new(vec![]);
assert!(executor.setup_envs(HashMap::new()).is_err());
}

#[test]
fn wasm_container_envs_are_left_to_the_shim() {
// arg0 is not an ELF/script, so the executor treats it as a Wasm container.
let spec = spec(&["/hello.wasm"], &["FOO=bar"]);
let executor = Executor::<TestShim>::new(vec![]);
executor.validate(&spec).unwrap();
let before: HashMap<_, _> = std::env::vars().collect();
executor.setup_envs(envs(&spec)).unwrap();
let after: HashMap<_, _> = std::env::vars().collect();
assert_eq!(
before, after,
"setup_envs must not touch the process env for Wasm"
);
}
}
Loading