1use std::borrow::Cow;
5use std::ffi::{CString, OsStr};
6use std::fs::create_dir_all;
7use std::os::unix::process::CommandExt;
8use std::process::Command;
9
10use anyhow::{Context, Result};
11use cap_std_ext::camino::Utf8Path;
12use rustix::mount::{
13 MountFlags, MountPropagationFlags, mount, mount_bind, mount_bind_recursive, mount_change,
14};
15use rustix::process::{chdir, chroot};
16use rustix::thread::{UnshareFlags, unshare_unsafe};
17
18use crate::CommandRunExt;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum BindMode {
24 Default,
29 Recursive,
32}
33
34#[derive(Debug)]
37pub struct ChrootCmd<'a> {
38 chroot_path: Cow<'a, Utf8Path>,
40 bind_mounts: Vec<(&'a str, &'a str, BindMode)>,
42 env_vars: Vec<(&'a str, &'a str)>,
44}
45
46impl<'a> ChrootCmd<'a> {
47 pub fn new(path: &'a Utf8Path) -> Self {
49 Self {
50 chroot_path: Cow::Borrowed(path),
51 bind_mounts: Vec::new(),
52 env_vars: Vec::new(),
53 }
54 }
55
56 pub fn bind(
60 mut self,
61 source: &'a impl AsRef<Utf8Path>,
62 target: &'a impl AsRef<Utf8Path>,
63 mode: BindMode,
64 ) -> Self {
65 self.bind_mounts
66 .push((source.as_ref().as_str(), target.as_ref().as_str(), mode));
67 self
68 }
69
70 pub fn setenv(mut self, key: &'a str, value: &'a str) -> Self {
75 self.env_vars.push((key, value));
76 self
77 }
78
79 pub fn set_default_path(self) -> Self {
82 self.setenv(
83 "PATH",
84 "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
85 )
86 }
87
88 fn build_command<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<Command> {
91 let mut args_iter = args.into_iter();
92 let program = args_iter
93 .next()
94 .context("ChrootCmd requires the program as the first arg")?;
95
96 let proc_target = self.chroot_path.join("proc");
98 let dev_target = self.chroot_path.join("dev");
99 let sys_target = self.chroot_path.join("sys");
100 let run_target = self.chroot_path.join("run");
101 for p in [&proc_target, &dev_target, &sys_target, &run_target] {
102 create_dir_all(p).with_context(|| format!("Creating {p}"))?;
103 }
104
105 let proc_target = CString::new(proc_target.as_str())?;
108 let dev_target = CString::new(dev_target.as_str())?;
109 let sys_target = CString::new(sys_target.as_str())?;
110 let run_target = CString::new(run_target.as_str())?;
111
112 let user_binds: Vec<(CString, CString, BindMode)> = self
113 .bind_mounts
114 .iter()
115 .map(|(src, tgt, mode)| -> Result<_> {
116 let tgt_in_chroot = self.chroot_path.join(tgt.trim_start_matches('/'));
117 create_dir_all(&tgt_in_chroot)
118 .with_context(|| format!("Creating bind target {tgt_in_chroot}"))?;
119 Ok((
120 CString::new(*src)?,
121 CString::new(tgt_in_chroot.as_str())?,
122 *mode,
123 ))
124 })
125 .collect::<Result<_>>()?;
126
127 let chroot_cstr = CString::new(self.chroot_path.as_str())?;
128
129 let mut cmd = Command::new(program);
130 cmd.args(args_iter);
131 cmd.env_clear().envs(self.env_vars.iter().copied());
132
133 #[allow(unsafe_code)]
137 unsafe {
138 cmd.pre_exec(move || {
139 unshare_unsafe(UnshareFlags::NEWNS)?;
140
141 mount_change(
146 c"/",
147 MountPropagationFlags::PRIVATE | MountPropagationFlags::REC,
148 )?;
149
150 mount_bind_recursive(chroot_cstr.as_c_str(), chroot_cstr.as_c_str())?;
158
159 mount(
162 c"proc",
163 proc_target.as_c_str(),
164 c"proc",
165 MountFlags::empty(),
166 None,
167 )?;
168 mount_bind_recursive(c"/dev", dev_target.as_c_str())?;
169 mount_bind_recursive(c"/sys", sys_target.as_c_str())?;
170 mount_bind_recursive(c"/run", run_target.as_c_str())?;
174
175 for (src, tgt, mode) in &user_binds {
176 match mode {
177 BindMode::Recursive => {
178 mount_bind_recursive(src.as_c_str(), tgt.as_c_str())?
179 }
180 BindMode::Default => mount_bind(src.as_c_str(), tgt.as_c_str())?,
181 }
182 }
183
184 chroot(chroot_cstr.as_c_str())?;
185 chdir(c"/")?;
186
187 Ok(())
188 });
189 }
190
191 Ok(cmd)
192 }
193
194 pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
197 self.build_command(args)?
198 .log_debug()
199 .run_inherited_with_cmd_context()
200 }
201
202 pub fn run_get_string<S: AsRef<OsStr>>(
206 self,
207 args: impl IntoIterator<Item = S>,
208 ) -> Result<String> {
209 self.build_command(args)?.log_debug().run_get_string()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use cap_std_ext::camino::Utf8PathBuf;
217
218 fn tmp_root() -> (tempfile::TempDir, Utf8PathBuf) {
219 let dir = tempfile::tempdir().unwrap();
220 let path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap();
221 (dir, path)
222 }
223
224 #[test]
225 fn builder_accumulates_binds_and_env() {
226 let (_keep, root) = tmp_root();
227 let src = root.join("src");
228 let non_recursive_src = root.join("non-recursive-src");
229 let cmd = ChrootCmd::new(&root)
230 .bind(&src, &"/boot", BindMode::Recursive)
231 .bind(&non_recursive_src, &"/sysroot", BindMode::Default)
232 .setenv("FOO", "bar")
233 .set_default_path();
234 assert_eq!(cmd.bind_mounts.len(), 2);
235 assert_eq!(
236 cmd.bind_mounts[0],
237 (src.as_str(), "/boot", BindMode::Recursive)
238 );
239 assert_eq!(
240 cmd.bind_mounts[1],
241 (non_recursive_src.as_str(), "/sysroot", BindMode::Default)
242 );
243 assert_eq!(cmd.env_vars.len(), 2);
245 assert!(cmd.env_vars.iter().any(|(k, _)| *k == "PATH"));
246 assert!(cmd.env_vars.iter().any(|(k, v)| *k == "FOO" && *v == "bar"));
247 }
248
249 #[test]
250 fn build_command_creates_api_mount_dirs() {
251 let (_keep, root) = tmp_root();
252 let cmd = ChrootCmd::new(&root).build_command(["/bin/true"]).unwrap();
254 for sub in ["proc", "dev", "sys", "run"] {
255 assert!(
256 root.join(sub).is_dir(),
257 "API mount dir {sub} not created in {root}"
258 );
259 }
260 assert_eq!(cmd.get_program(), "/bin/true");
261 }
262
263 #[test]
264 fn build_command_creates_user_bind_targets() {
265 let (_keep, root) = tmp_root();
266 let (_keep2, src_root) = tmp_root();
267 ChrootCmd::new(&root)
268 .bind(&src_root, &"/sysroot", BindMode::Recursive)
269 .build_command(["/bin/true"])
270 .unwrap();
271 assert!(root.join("sysroot").is_dir());
272 }
273
274 #[test]
275 fn build_command_rejects_empty_args() {
276 let (_keep, root) = tmp_root();
277 let err = ChrootCmd::new(&root)
278 .build_command(std::iter::empty::<&str>())
279 .unwrap_err();
280 assert!(
281 err.to_string().contains("ChrootCmd requires the program"),
282 "unexpected error: {err}"
283 );
284 }
285}