Skip to main content

kernel_abi_tools/
container.rs

1//! Runs a program in a container whose seccomp profile simulates a kernel.
2
3use crate::{seccomp_profile, Target};
4use std::env;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// The container engine and the options added to its command line.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Engine {
13    /// `podman` or `docker`.
14    pub program: String,
15    /// Options placed before `run`, e.g. `--runtime crun`.
16    pub engine_args: Vec<String>,
17    /// Options added to `run`, e.g. `--network none`.
18    pub run_args: Vec<String>,
19}
20
21impl Engine {
22    /// Reads `CONTAINER_ENGINE` (default `podman`), `KERNEL_ABI_ENGINE_ARGS`,
23    /// and `KERNEL_ABI_RUN_ARGS` (whitespace-separated).
24    pub fn from_env() -> Self {
25        Engine {
26            program: env::var("CONTAINER_ENGINE").unwrap_or_else(|_| "podman".to_string()),
27            engine_args: env_words("KERNEL_ABI_ENGINE_ARGS"),
28            run_args: env_words("KERNEL_ABI_RUN_ARGS"),
29        }
30    }
31}
32
33/// Runs `program` with `args` in `image` under the profile for `target` and
34/// returns its exit status (128 if it was killed by a signal).
35///
36/// The current directory, and `program` if it is a host path outside it, are
37/// mounted at their own absolute paths so relative paths and compile-time
38/// `CARGO_MANIFEST_DIR` values keep working. A bare `program` name (no `/`)
39/// runs from the image. A host program is mounted as a single file, never its
40/// directory, which could hide the image's own `/bin` or `/usr/bin`.
41/// `CARGO*`/`RUST*` variables are forwarded. Suitable for a Cargo runner.
42///
43/// # Errors
44///
45/// Returns a message when `image` is empty, the current directory cannot be
46/// read, a host `program` is not a file, the profile cannot be written to the
47/// temporary directory, or the engine cannot be started. The program's own
48/// failure is its exit status, not an error.
49pub fn run(
50    engine: &Engine,
51    target: &Target,
52    image: &str,
53    program: &str,
54    args: &[String],
55) -> Result<i32, String> {
56    if image.is_empty() {
57        return Err("no container image selected".to_string());
58    }
59    let cwd = env::current_dir().map_err(|e| format!("current directory: {e}"))?;
60    let host_program = program
61        .contains('/')
62        .then(|| absolute(&cwd, Path::new(program)));
63    if let Some(p) = &host_program {
64        if !p.is_file() {
65            return Err(format!("{} is not a file on this host", p.display()));
66        }
67    }
68
69    let profile = TempFile::new("kernel-seccomp", ".json")?;
70    fs::write(&profile.0, seccomp_profile(target))
71        .map_err(|e| format!("{}: {e}", profile.0.display()))?;
72
73    let mut cmd = Command::new(&engine.program);
74    cmd.args(&engine.engine_args);
75    cmd.args(["run", "--rm", "--security-opt"]);
76    cmd.arg(format!("seccomp={}", profile.0.display()));
77    cmd.arg("-v").arg(format!("{0}:{0}:z", cwd.display()));
78    match &host_program {
79        Some(p) if !p.starts_with(&cwd) => {
80            cmd.arg("-v").arg(format!("{0}:{0}:ro,z", p.display()));
81        }
82        _ => {}
83    }
84    cmd.arg("-w").arg(&cwd);
85    cmd.args(["-e", "CARGO*", "-e", "RUST*"]);
86    cmd.args(&engine.run_args);
87    cmd.arg(image);
88    match &host_program {
89        Some(p) => cmd.arg(p),
90        None => cmd.arg(program),
91    };
92    cmd.args(args);
93
94    let status = cmd
95        .status()
96        .map_err(|e| format!("cannot start {}: {e}", engine.program))?;
97    Ok(status.code().unwrap_or(128))
98}
99
100fn env_words(name: &str) -> Vec<String> {
101    env::var(name)
102        .unwrap_or_default()
103        .split_whitespace()
104        .map(str::to_string)
105        .collect()
106}
107
108fn absolute(cwd: &Path, path: &Path) -> PathBuf {
109    if path.is_absolute() {
110        path.to_path_buf()
111    } else {
112        cwd.join(path)
113    }
114}
115
116/// A file in the temporary directory, removed when dropped.
117struct TempFile(PathBuf);
118
119impl TempFile {
120    fn new(prefix: &str, suffix: &str) -> Result<Self, String> {
121        let nanos = SystemTime::now()
122            .duration_since(UNIX_EPOCH)
123            .map(|d| d.as_nanos())
124            .unwrap_or(0);
125        let name = format!("{prefix}-{}-{nanos}{suffix}", std::process::id());
126        let path = env::temp_dir().join(name);
127        fs::OpenOptions::new()
128            .write(true)
129            .create_new(true)
130            .open(&path)
131            .map_err(|e| format!("{}: {e}", path.display()))?;
132        Ok(TempFile(path))
133    }
134}
135
136impl Drop for TempFile {
137    fn drop(&mut self) {
138        let _ = fs::remove_file(&self.0);
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::KernelVersion;
146
147    #[test]
148    fn rejects_missing_image_and_missing_host_program() {
149        let engine = Engine {
150            program: "definitely-not-a-container-engine".to_string(),
151            engine_args: Vec::new(),
152            run_args: Vec::new(),
153        };
154        let target = Target::kernel(KernelVersion::parse("4.12").unwrap());
155        assert!(run(&engine, &target, "", "sh", &[]).is_err());
156        let err = run(&engine, &target, "img", "./no/such/program", &[]).unwrap_err();
157        assert!(err.contains("not a file on this host"), "{err}");
158        let err = run(&engine, &target, "img", "sh", &[]).unwrap_err();
159        assert!(err.contains("cannot start"), "{err}");
160    }
161}