kernel_abi_tools/
container.rs1use 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#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Engine {
13 pub program: String,
15 pub engine_args: Vec<String>,
17 pub run_args: Vec<String>,
19}
20
21impl Engine {
22 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
33pub 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
116struct 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}