runtime/tools/exec/confinement/
placing.rs1use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13use kernel::{AxCode, AxError};
14
15use crate::backlog::{BacklogId, Finished};
16
17use super::{Confinement, Missing, Offerings};
18
19pub struct Confined {
21 arm: Confinement,
22 scratch: Option<PathBuf>,
23 idle: Option<Mirror>,
24 outstanding: BTreeMap<BacklogId, Mirror>,
25}
26
27struct Mirror {
29 of: PathBuf,
30 at: PathBuf,
31}
32
33pub struct Placed {
35 copy: Option<Mirror>,
36 work: storage::FileWork,
37}
38
39impl Placed {
40 pub(in crate::tools::exec) fn into_container_copy(self) -> Result<PathBuf, AxError> {
41 self.copy
42 .map(|copy| copy.at)
43 .ok_or_else(|| no_arm(Missing::ScratchDirectory))
44 }
45
46 pub fn work(&self) -> storage::FileWork {
49 self.work
50 }
51}
52
53impl Confined {
54 pub fn detect() -> Confined {
56 let offered = Offerings::this_machine();
57 Confined {
58 arm: Confinement::choose(&offered),
59 scratch: offered.scratch,
60 idle: None,
61 outstanding: BTreeMap::new(),
62 }
63 }
64
65 pub fn with_arm(arm: Confinement, scratch: Option<PathBuf>) -> Confined {
68 Confined {
69 arm,
70 scratch,
71 idle: None,
72 outstanding: BTreeMap::new(),
73 }
74 }
75
76 pub fn arm(&self) -> &Confinement {
77 &self.arm
78 }
79
80 pub fn statement(&self) -> String {
83 self.arm.statement()
84 }
85
86 pub fn place(
97 &mut self,
98 mut command: Command,
99 workdir: &Path,
100 ) -> Result<(Command, Placed), AxError> {
101 let wrapper = match &self.arm {
102 Confinement::Unavailable { missing } => return Err(no_arm(*missing)),
103 Confinement::WindowsJobObject => {
104 #[cfg(not(windows))]
105 return Err(no_job_object());
106 #[cfg(windows)]
107 {
108 None
109 }
110 }
111 Confinement::MacosSeatbelt { .. } => {
112 #[cfg(target_os = "macos")]
113 {
114 None
115 }
116 #[cfg(not(target_os = "macos"))]
117 {
118 return Err(AxError::failure(
119 AxCode::SandboxDenied,
120 "initialize macOS Seatbelt confinement",
121 "this host is not macOS",
122 )
123 .with_recovery("select a native sandbox for this host platform"));
124 }
125 }
126 Confinement::LinuxNamespaces { wrapper } => {
127 #[cfg(target_os = "linux")]
128 {
129 let denied = |detail| {
130 AxError::failure(
131 AxCode::SandboxDenied,
132 "create the Linux confinement",
133 format!(
134 "this arm requires an unprivileged user namespace; \
135 namespace setup failed: {detail}"
136 ),
137 )
138 .with_recovery(
139 "use a Linux machine that permits unprivileged user namespaces, \
140 or ask the User to choose `where: host`",
141 )
142 };
143 let probe = namespaced(wrapper, workdir, workdir, &child::command("/bin/true"))
144 .output()
145 .map_err(|err| denied(err.to_string()))?;
146 if !probe.status.success() {
147 return Err(denied(format!(
148 "{}: {}",
149 probe.status,
150 String::from_utf8_lossy(&probe.stderr).trim()
151 )));
152 }
153 }
154 Some(wrapper.clone())
155 }
156 Confinement::CopiedTree => None,
157 };
158 let (copy, work) = self.synced(workdir)?;
159 #[cfg(target_os = "macos")]
160 if let Confinement::MacosSeatbelt { wrapper } = &self.arm {
161 let native = super::super::native_macos::probe(wrapper, ©.at)
162 .and_then(|()| super::super::native_macos::wrap(wrapper, ©.at, &command));
163 return match native {
164 Ok(command) => Ok((
165 command,
166 Placed {
167 copy: Some(copy),
168 work,
169 },
170 )),
171 Err(error) => {
172 remove(©.at);
173 Err(error)
174 }
175 };
176 }
177 let command = match wrapper {
178 Some(wrapper) => namespaced(&wrapper, ©.at, workdir, &command),
179 None => {
180 command.current_dir(©.at);
181 command
182 }
183 };
184 Ok((
185 command,
186 Placed {
187 copy: Some(copy),
188 work,
189 },
190 ))
191 }
192
193 pub fn settled(&mut self, placed: Placed) {
195 if let Some(copy) = placed.copy {
196 self.keep(copy);
197 }
198 }
199
200 pub fn handed(&mut self, id: BacklogId, placed: Placed) {
202 if let Some(copy) = placed.copy {
203 self.outstanding.insert(id, copy);
204 }
205 }
206
207 pub fn reaped(&mut self, finished: &[Finished]) {
209 for member in finished {
210 if let Some(copy) = self.outstanding.remove(&member.id) {
211 self.keep(copy);
212 }
213 }
214 }
215
216 fn keep(&mut self, copy: Mirror) {
225 match self.idle {
226 None => self.idle = Some(copy),
227 Some(_) => remove(©.at),
228 }
229 }
230
231 fn synced(&mut self, workdir: &Path) -> Result<(Mirror, storage::FileWork), AxError> {
234 let root = self
235 .scratch
236 .as_deref()
237 .ok_or_else(|| no_arm(Missing::ScratchDirectory))?;
238 let copy = match self.idle.take() {
239 Some(kept) if kept.of == workdir => kept,
240 Some(other) => {
241 remove(&other.at);
242 Mirror {
243 of: workdir.to_path_buf(),
244 at: fresh(root)?,
245 }
246 }
247 None => Mirror {
248 of: workdir.to_path_buf(),
249 at: fresh(root)?,
250 },
251 };
252 let mut budget = Budget::new();
253 match mirror(
254 &Stage {
255 from: workdir,
256 into: ©.at,
257 copy: ©.at,
258 },
259 0,
260 &mut budget,
261 ) {
262 Ok(()) => Ok((copy, budget.work())),
263 Err(fault) => {
264 remove(©.at);
270 Err(fault)
271 }
272 }
273 }
274}
275
276impl Drop for Confined {
277 fn drop(&mut self) {
278 for copy in self.idle.iter().chain(self.outstanding.values()) {
284 remove(©.at);
285 }
286 }
287}
288
289fn no_arm(missing: Missing) -> AxError {
291 AxError::failure(
292 AxCode::SandboxDenied,
293 "run outside the confinement",
294 missing.phrase(),
295 )
296 .with_recovery(missing.recovery())
297}
298
299#[cfg(not(windows))]
301fn no_job_object() -> AxError {
302 AxError::failure(
303 AxCode::SandboxDenied,
304 "run under a job object",
305 "this build makes no job object",
306 )
307 .with_recovery(
308 "run under the copied tree instead: it copies the working directory and does \
309 not isolate the network, so keep the command off the network or run it on a \
310 machine that has the arm",
311 )
312}
313
314pub(super) mod copy;
315
316use copy::{Budget, Stage, fresh, mirror, remove};
317
318fn namespaced(wrapper: &Path, copy: &Path, workdir: &Path, command: &Command) -> Command {
322 let mut wrapped = child::command(wrapper);
323 wrapped
324 .current_dir(workdir)
325 .arg("--ro-bind")
326 .arg("/")
327 .arg("/")
328 .arg("--bind")
329 .arg(copy)
330 .arg(workdir)
331 .arg("--chdir")
332 .arg(workdir)
333 .arg("--proc")
334 .arg("/proc")
335 .arg("--dev")
336 .arg("/dev")
337 .arg("--unshare-all");
338 #[cfg(target_os = "linux")]
339 wrapped.arg("--unshare-user");
340 wrapped
341 .arg("--die-with-parent")
342 .arg("--")
343 .arg(command.get_program());
344 for arg in command.get_args() {
345 wrapped.arg(arg);
346 }
347 for (name, value) in command.get_envs() {
348 match value {
349 Some(value) => wrapped.env(name, value),
350 None => wrapped.env_remove(name),
351 };
352 }
353 wrapped
354}