bootc_internal_utils/
chroot.rs1use 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::{MountFlags, MountPropagationFlags, mount, mount_bind_recursive, mount_change};
13use rustix::process::{chdir, chroot};
14use rustix::thread::{UnshareFlags, unshare_unsafe};
15
16use crate::CommandRunExt;
17
18#[derive(Debug)]
21pub struct ChrootCmd<'a> {
22 chroot_path: Cow<'a, Utf8Path>,
24 bind_mounts: Vec<(&'a str, &'a str)>,
26 env_vars: Vec<(&'a str, &'a str)>,
28}
29
30impl<'a> ChrootCmd<'a> {
31 pub fn new(path: &'a Utf8Path) -> Self {
33 Self {
34 chroot_path: Cow::Borrowed(path),
35 bind_mounts: Vec::new(),
36 env_vars: Vec::new(),
37 }
38 }
39
40 pub fn bind(
43 mut self,
44 source: &'a impl AsRef<Utf8Path>,
45 target: &'a impl AsRef<Utf8Path>,
46 ) -> Self {
47 self.bind_mounts
48 .push((source.as_ref().as_str(), target.as_ref().as_str()));
49 self
50 }
51
52 pub fn setenv(mut self, key: &'a str, value: &'a str) -> Self {
57 self.env_vars.push((key, value));
58 self
59 }
60
61 pub fn set_default_path(self) -> Self {
64 self.setenv(
65 "PATH",
66 "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
67 )
68 }
69
70 fn build_command<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<Command> {
73 let mut args_iter = args.into_iter();
74 let program = args_iter
75 .next()
76 .context("ChrootCmd requires the program as the first arg")?;
77
78 let proc_target = self.chroot_path.join("proc");
80 let dev_target = self.chroot_path.join("dev");
81 let sys_target = self.chroot_path.join("sys");
82 let run_target = self.chroot_path.join("run");
83 for p in [&proc_target, &dev_target, &sys_target, &run_target] {
84 create_dir_all(p).with_context(|| format!("Creating {p}"))?;
85 }
86
87 let proc_target = CString::new(proc_target.as_str())?;
90 let dev_target = CString::new(dev_target.as_str())?;
91 let sys_target = CString::new(sys_target.as_str())?;
92 let run_target = CString::new(run_target.as_str())?;
93
94 let user_binds: Vec<(CString, CString)> = self
95 .bind_mounts
96 .iter()
97 .map(|(src, tgt)| -> Result<_> {
98 let tgt_in_chroot = self.chroot_path.join(tgt.trim_start_matches('/'));
99 create_dir_all(&tgt_in_chroot)
100 .with_context(|| format!("Creating bind target {tgt_in_chroot}"))?;
101 Ok((CString::new(*src)?, CString::new(tgt_in_chroot.as_str())?))
102 })
103 .collect::<Result<_>>()?;
104
105 let chroot_cstr = CString::new(self.chroot_path.as_str())?;
106
107 let mut cmd = Command::new(program);
108 cmd.args(args_iter);
109 cmd.env_clear().envs(self.env_vars.iter().copied());
110
111 #[allow(unsafe_code)]
115 unsafe {
116 cmd.pre_exec(move || {
117 unshare_unsafe(UnshareFlags::NEWNS)?;
118
119 mount_change(
124 c"/",
125 MountPropagationFlags::PRIVATE | MountPropagationFlags::REC,
126 )?;
127
128 mount_bind_recursive(chroot_cstr.as_c_str(), chroot_cstr.as_c_str())?;
136
137 mount(
140 c"proc",
141 proc_target.as_c_str(),
142 c"proc",
143 MountFlags::empty(),
144 None,
145 )?;
146 mount_bind_recursive(c"/dev", dev_target.as_c_str())?;
147 mount_bind_recursive(c"/sys", sys_target.as_c_str())?;
148 mount_bind_recursive(c"/run", run_target.as_c_str())?;
152
153 for (src, tgt) in &user_binds {
154 mount_bind_recursive(src.as_c_str(), tgt.as_c_str())?;
155 }
156
157 chroot(chroot_cstr.as_c_str())?;
158 chdir(c"/")?;
159
160 Ok(())
161 });
162 }
163
164 Ok(cmd)
165 }
166
167 pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
170 self.build_command(args)?
171 .log_debug()
172 .run_inherited_with_cmd_context()
173 }
174
175 pub fn run_get_string<S: AsRef<OsStr>>(
179 self,
180 args: impl IntoIterator<Item = S>,
181 ) -> Result<String> {
182 self.build_command(args)?.log_debug().run_get_string()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use cap_std_ext::camino::Utf8PathBuf;
190
191 fn tmp_root() -> (tempfile::TempDir, Utf8PathBuf) {
192 let dir = tempfile::tempdir().unwrap();
193 let path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap();
194 (dir, path)
195 }
196
197 #[test]
198 fn builder_accumulates_binds_and_env() {
199 let (_keep, root) = tmp_root();
200 let src = root.join("src");
201 let cmd = ChrootCmd::new(&root)
202 .bind(&src, &"/boot")
203 .setenv("FOO", "bar")
204 .set_default_path();
205 assert_eq!(cmd.bind_mounts.len(), 1);
206 assert_eq!(cmd.bind_mounts[0].1, "/boot");
207 assert_eq!(cmd.env_vars.len(), 2);
209 assert!(cmd.env_vars.iter().any(|(k, _)| *k == "PATH"));
210 assert!(cmd.env_vars.iter().any(|(k, v)| *k == "FOO" && *v == "bar"));
211 }
212
213 #[test]
214 fn build_command_creates_api_mount_dirs() {
215 let (_keep, root) = tmp_root();
216 let cmd = ChrootCmd::new(&root).build_command(["/bin/true"]).unwrap();
218 for sub in ["proc", "dev", "sys", "run"] {
219 assert!(
220 root.join(sub).is_dir(),
221 "API mount dir {sub} not created in {root}"
222 );
223 }
224 assert_eq!(cmd.get_program(), "/bin/true");
225 }
226
227 #[test]
228 fn build_command_creates_user_bind_targets() {
229 let (_keep, root) = tmp_root();
230 let (_keep2, src_root) = tmp_root();
231 ChrootCmd::new(&root)
232 .bind(&src_root, &"/sysroot")
233 .build_command(["/bin/true"])
234 .unwrap();
235 assert!(root.join("sysroot").is_dir());
236 }
237
238 #[test]
239 fn build_command_rejects_empty_args() {
240 let (_keep, root) = tmp_root();
241 let err = ChrootCmd::new(&root)
242 .build_command(std::iter::empty::<&str>())
243 .unwrap_err();
244 assert!(
245 err.to_string().contains("ChrootCmd requires the program"),
246 "unexpected error: {err}"
247 );
248 }
249}