/build/source/nativelink-worker/src/namespace_utils.rs
Line | Count | Source |
1 | | // Copyright 2026 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use std::io::Error; |
16 | | |
17 | | use tracing::{error, warn}; |
18 | | |
19 | | /// A wrapper around a Child that may be the stub of a PID namespace. A |
20 | | /// SIGTERM to the stub is passed on to the action; a SIGKILL to the stub |
21 | | /// ends the whole namespace, which is the hard kill. |
22 | | #[derive(Debug)] |
23 | | pub struct MaybeNamespacedChild { |
24 | | namespaced: bool, |
25 | | child: tokio::process::Child, |
26 | | } |
27 | | |
28 | | /// SIGKILL to every process in the group `pgid` leads. ESRCH means the |
29 | | /// group is already gone, which is the outcome wanted; anything else, EPERM |
30 | | /// above all, means a survivor and is reported. |
31 | | #[cfg(target_os = "linux")] |
32 | 4 | fn kill_process_group(pgid: u32) { |
33 | 4 | let Ok(pgid) = i32::try_from(pgid) else { |
34 | 0 | return; |
35 | | }; |
36 | | // SAFETY: killpg only takes integers and has no memory safety |
37 | | // considerations; a stale group id is reported as ESRCH, not acted on. |
38 | 4 | let rc = unsafe { libc::killpg(pgid, libc::SIGKILL) }; |
39 | 4 | if rc != 0 { |
40 | 1 | let err = Error::last_os_error(); |
41 | 1 | if err.raw_os_error() != Some(libc::ESRCH) { |
42 | 0 | error!( |
43 | | pgid, |
44 | | ?err, |
45 | | "Could not kill the action's process group; a descendant may have survived" |
46 | | ); |
47 | 1 | } |
48 | 3 | } |
49 | 4 | } |
50 | | |
51 | | impl MaybeNamespacedChild { |
52 | 0 | pub const fn new(namespaced: bool, child: tokio::process::Child) -> Self { |
53 | 0 | Self { namespaced, child } |
54 | 0 | } |
55 | | |
56 | 104 | pub fn id(&self) -> Option<u32> { |
57 | 104 | self.child.id() |
58 | 104 | } |
59 | | |
60 | | /// SIGKILL the child and wait for it. Namespaced, the child is the |
61 | | /// stub and the init of the action's PID namespace, so its death takes |
62 | | /// the action and everything the action spawned with it; the stub also |
63 | | /// set `PR_SET_PDEATHSIG` on the action for the same end. Not |
64 | | /// namespaced, the child is its own group leader, so the group gets the |
65 | | /// SIGKILL first: a timed-out or cancelled action used to lose only its |
66 | | /// direct child while its descendants ran on (issue #225). |
67 | 14 | pub async fn kill(&mut self) -> Result<(), Error> { |
68 | | #[cfg(target_os = "linux")] |
69 | 14 | if !self.namespaced |
70 | 4 | && let Some(pgid) = self.child.id() |
71 | 4 | { |
72 | 4 | kill_process_group(pgid); |
73 | 10 | } |
74 | 14 | self.child.kill().await |
75 | 14 | } |
76 | | |
77 | 2 | pub fn try_wait(&mut self) -> Result<Option<std::process::ExitStatus>, Error> { |
78 | 2 | self.child.try_wait() |
79 | 2 | } |
80 | | |
81 | 62 | pub async fn wait(&mut self) -> Result<std::process::ExitStatus, Error>0 { |
82 | 62 | self.child.wait().await |
83 | 51 | } |
84 | | } |
85 | | |
86 | 0 | fn exit(status: i32) -> ! { |
87 | | // SAFETY: It is always safe to _exit. |
88 | 0 | unsafe { libc::_exit(status) }; |
89 | | } |
90 | | |
91 | | enum NamespaceErrorType { |
92 | | Unshare = 1, |
93 | | WriteSignalSafe, |
94 | | Mount, |
95 | | } |
96 | | |
97 | | const NS_ERROR_TYPE_BITS: u8 = 2; // This is 2 because the highest value (NamespaceErrorType::Mount) is 3 and so we can store all of this in two bits |
98 | | const NS_ERROR_TYPE_MASK: i32 = 0x3; // 11 - i.e. NS_ERROR_TYPE_BITS lowest bits |
99 | | |
100 | | /// The exit code a probe child could not be forked with: no step of the |
101 | | /// probe ran, so there is no errno to carry. |
102 | | const PROBE_FORK_FAILED: i32 = 255; |
103 | | |
104 | | /// The namespaces an action is run in. The mount namespace is among them, |
105 | | /// private to the action: the fresh `/proc` needs one, and without |
106 | | /// `use_mount_namespace` nothing else in it changes, so there it is |
107 | | /// optional (see [`unshare_action_namespaces`]). |
108 | | const ACTION_NAMESPACES: libc::c_int = libc::CLONE_NEWPID |
109 | | | libc::CLONE_NEWUSER |
110 | | | libc::CLONE_NEWIPC |
111 | | | libc::CLONE_NEWUTS |
112 | | | libc::CLONE_NEWNS; |
113 | | |
114 | | /// Whether an errno is a refusal by policy, which a seccomp filter or a |
115 | | /// capability check answers with, rather than a failure. |
116 | 0 | const fn is_denial(errno: i32) -> bool { |
117 | 0 | errno == libc::EPERM || errno == libc::EACCES |
118 | 0 | } |
119 | | |
120 | | /// Enters the namespaces an action runs in and says whether the mount |
121 | | /// namespace is among them. With `mount` it has to be. Without it, the |
122 | | /// mount namespace only carries the fresh `/proc`, so a policy that |
123 | | /// refuses it gets the namespaces an action had before the fresh `/proc` |
124 | | /// existed, and the action keeps the host's `/proc`. On failure the error |
125 | | /// is the errno. Async-signal-safe. |
126 | 0 | fn unshare_action_namespaces(mount: bool) -> Result<bool, i32> { |
127 | | // SAFETY: Unshare does not have any unsafe effects and modifies no |
128 | | // memory, it is also async-signal-safe. |
129 | 0 | if unsafe { libc::unshare(ACTION_NAMESPACES) } == 0 { |
130 | 0 | return Ok(true); |
131 | 0 | } |
132 | | // SAFETY: We just called a libc function that failed (-1). |
133 | 0 | let errno = unsafe { *libc::__errno_location() }; |
134 | 0 | if mount { |
135 | 0 | return Err(errno); |
136 | 0 | } |
137 | | // SAFETY: As above; a failed unshare has no effect to undo. |
138 | 0 | if unsafe { libc::unshare(ACTION_NAMESPACES & !libc::CLONE_NEWNS) } == 0 { |
139 | 0 | return Ok(false); |
140 | 0 | } |
141 | | // SAFETY: We just called a libc function that failed (-1). |
142 | 0 | Err(unsafe { *libc::__errno_location() }) |
143 | 0 | } |
144 | | |
145 | | /// Makes the mounts of the new mount namespace private, so nothing |
146 | | /// mounted in it reaches the host, and says whether it did. With `mount` |
147 | | /// it has to. Without it, a refusal by policy is tolerated, as the mount |
148 | | /// namespace is then only there for the fresh `/proc`, which is not |
149 | | /// mounted either. Async-signal-safe. |
150 | 0 | fn privatize_mounts(mount: bool) -> Result<bool, Error> { |
151 | 0 | match make_mounts_private() { |
152 | 0 | Ok(()) => Ok(true), |
153 | 0 | Err(err) if !mount && err.raw_os_error().is_some_and(is_denial) => Ok(false), |
154 | 0 | Err(err) => Err(err), |
155 | | } |
156 | 0 | } |
157 | | |
158 | | /// Encodes which step of a probe failed, and with what errno, as the exit |
159 | | /// code its child reports. |
160 | 0 | const fn probe_exit_code(step: NamespaceErrorType, errno: i32) -> i32 { |
161 | 0 | (step as i32) | (errno << NS_ERROR_TYPE_BITS) |
162 | 0 | } |
163 | | |
164 | | /// The child side of a probe: enters the namespaces an action is given, |
165 | | /// writes the id maps `configure_namespace` writes, in its order and with |
166 | | /// its tolerance, and makes the new mount namespace private, the last two |
167 | | /// with the tolerance `mount` decides. The gid has to be mapped too: an |
168 | | /// inode created on a filesystem mounted inside the namespace, as the |
169 | | /// probe's mkdir does, needs the creator's group to map, or the kernel |
170 | | /// refuses it with EOVERFLOW. On failure the error is the exit code to |
171 | | /// report it with. Async-signal-safe. |
172 | 0 | fn enter_probe_namespaces(mount: bool, uid_map: &[u8], gid_map: &[u8]) -> Result<(), i32> { |
173 | 0 | let has_mount_namespace = unshare_action_namespaces(mount) |
174 | 0 | .map_err(|errno| probe_exit_code(NamespaceErrorType::Unshare, errno))?; |
175 | 0 | if let Err(err) = write_signal_safe(c"/proc/self/setgroups", b"deny") |
176 | 0 | && err != libc::EPERM |
177 | 0 | && err != libc::EACCES |
178 | 0 | && err != libc::ENOENT |
179 | | { |
180 | 0 | return Err(probe_exit_code(NamespaceErrorType::WriteSignalSafe, err)); |
181 | 0 | } |
182 | 0 | if let Err(err) = write_signal_safe(c"/proc/self/uid_map", uid_map) { |
183 | 0 | return Err(probe_exit_code(NamespaceErrorType::WriteSignalSafe, err)); |
184 | 0 | } |
185 | 0 | if let Err(err) = write_signal_safe(c"/proc/self/gid_map", gid_map) |
186 | 0 | && err != libc::EPERM |
187 | 0 | && err != libc::EACCES |
188 | | { |
189 | 0 | return Err(probe_exit_code(NamespaceErrorType::WriteSignalSafe, err)); |
190 | 0 | } |
191 | 0 | if has_mount_namespace && let Err(err) = privatize_mounts(mount) { |
192 | 0 | let errno = err.raw_os_error().unwrap_or(libc::EIO); |
193 | 0 | return Err(probe_exit_code(NamespaceErrorType::Mount, errno)); |
194 | 0 | } |
195 | 0 | Ok(()) |
196 | 0 | } |
197 | | |
198 | | /// Waits for a probe child and returns its exit code, or `None` when it did |
199 | | /// not exit on its own; that failure is logged here. |
200 | 91 | fn wait_probe(pid: libc::pid_t) -> Option<i32> { |
201 | 91 | let mut status = 0; |
202 | | // SAFETY: The pid is valid and created by us and the status is our own stack. |
203 | 91 | while unsafe { libc::waitpid(pid, &raw mut status, 0) } == -1 { |
204 | | // SAFETY: We just called a libc function that failed (-1). |
205 | 0 | let errno = unsafe { *libc::__errno_location() }; |
206 | 0 | if errno != libc::EINTR { |
207 | 0 | error!(errno = errno, "Namespaces: Failure in waitpid"); |
208 | 0 | return None; |
209 | 0 | } |
210 | | } |
211 | 91 | if libc::WIFEXITED(status) { |
212 | 91 | return Some(libc::WEXITSTATUS(status)); |
213 | 0 | } |
214 | 0 | error!( |
215 | | exit_code = status, |
216 | | "Namespaces: waitpid exit with non-exit code" |
217 | | ); |
218 | 0 | None |
219 | 91 | } |
220 | | |
221 | | /// Determines whether the namespaces provided by this module are supported |
222 | | /// on the currently running system by forking a process and trying to enter |
223 | | /// it into the new namespaces. When `mount` is set the mount namespace is |
224 | | /// checked as well, and when `isolate_tmp` is also set the check includes |
225 | | /// bind mounting over `/tmp`. |
226 | 89 | pub fn namespaces_supported(mount: bool, isolate_tmp: bool) -> bool { |
227 | 89 | if isolate_tmp && !mount8 { |
228 | 1 | error!("Namespaces: isolating /tmp requires a mount namespace"); |
229 | 1 | return false; |
230 | 88 | } |
231 | 88 | if isolate_tmp && !std::path::Path::new("/tmp").is_dir()7 { |
232 | 0 | error!("Namespaces: isolating /tmp requires /tmp to exist and be a directory"); |
233 | 0 | return false; |
234 | 88 | } |
235 | | // SAFETY: Posix requires that geteuid and getegid are always successful. |
236 | 88 | let uid = unsafe { libc::geteuid() }; |
237 | | // SAFETY: As above. |
238 | 88 | let gid = unsafe { libc::getegid() }; |
239 | | // Read before the fork: once the child has unshared, its ids are the |
240 | | // unmapped overflow ids until the maps are written. |
241 | 88 | let uid_map = format!("{uid} {uid} 1\n"); |
242 | 88 | let gid_map = format!("{gid} {gid} 1\n"); |
243 | | // SAFETY: We ensure that if pid == 0 we only call async-signal-safe functions. |
244 | 88 | let pid = unsafe { libc::fork() }; |
245 | 88 | match pid { |
246 | | 0 => { |
247 | 0 | if let Err(code) = enter_probe_namespaces(mount, uid_map.as_bytes(), gid_map.as_bytes()) |
248 | | { |
249 | 0 | exit(code); |
250 | 0 | } |
251 | 0 | if !mount { |
252 | 0 | exit(0); |
253 | 0 | } |
254 | | // The same sequence `perform_remount` runs for an action, so a |
255 | | // policy that allows a bind to self but refuses a sized tmpfs, |
256 | | // or a mkdir under it, fails here and not in every action's |
257 | | // pre_exec. All of it is inside this child's private mount |
258 | | // namespace. |
259 | 0 | if isolate_tmp |
260 | 0 | && let Err(err) = bind_mount(c"/tmp", c"/tmp") |
261 | 0 | .and_then(|()| mount_tmpfs(c"/tmp", Some(ROOT_MASK_TMPFS_OPTIONS))) |
262 | 0 | .and_then(|()| mkdir_p_signal_safe(c"/tmp/nativelink-namespace-probe/action")) |
263 | | { |
264 | 0 | let errno = err.raw_os_error().unwrap_or(libc::EIO); |
265 | 0 | exit(probe_exit_code(NamespaceErrorType::Mount, errno)); |
266 | 0 | } |
267 | 0 | exit(0); |
268 | | } |
269 | 88 | pid if pid > 0 => { |
270 | 88 | match wait_probe(pid) { |
271 | 88 | Some(0) => return true, |
272 | 0 | Some(s) if s & NS_ERROR_TYPE_MASK == NamespaceErrorType::Unshare as i32 => { |
273 | 0 | let errno = s >> NS_ERROR_TYPE_BITS; |
274 | 0 | error!(errno, "Namespaces: Error during unshare"); |
275 | 0 | if errno == libc::EPERM { |
276 | 0 | error!( |
277 | | "If the worker is inside Docker, namespaces don't work unless it's a privileged container" |
278 | | ); |
279 | 0 | } |
280 | | } |
281 | 0 | Some(s) if s & NS_ERROR_TYPE_MASK == NamespaceErrorType::WriteSignalSafe as i32 => { |
282 | 0 | error!( |
283 | 0 | errno = s >> NS_ERROR_TYPE_BITS, |
284 | | "Namespaces: Error while writing the id maps under /proc/self" |
285 | | ); |
286 | | } |
287 | 0 | Some(s) if s & NS_ERROR_TYPE_MASK == NamespaceErrorType::Mount as i32 => { |
288 | 0 | error!( |
289 | 0 | errno = s >> NS_ERROR_TYPE_BITS, |
290 | | "Failure to mount during namespace checking" |
291 | | ); |
292 | | } |
293 | 0 | Some(other) => { |
294 | 0 | error!( |
295 | | exit_code = other, |
296 | | "Namespace check failure with unknown exit code" |
297 | | ); |
298 | | } |
299 | 0 | None => {} |
300 | | } |
301 | 0 | false |
302 | | } |
303 | 0 | _ => false, |
304 | | } |
305 | 89 | } |
306 | | |
307 | | /// Whether a fresh `proc` can be mounted over `/proc` inside an action's |
308 | | /// PID namespace here, so that `/proc` lists the action's processes under |
309 | | /// the PIDs the action knows them by. When it cannot, the worker runs |
310 | | /// actions with the host's `/proc` instead, after the warning this logs. An |
311 | | /// unprivileged container that masks paths under `/proc` is the usual |
312 | | /// reason: the kernel refuses a `proc` mount in a user namespace that would |
313 | | /// reveal what those masks hide. A syscall policy that denies `mount`, or |
314 | | /// the mount namespace itself, is the other. |
315 | 3 | pub fn fresh_proc_supported() -> bool { |
316 | | // SAFETY: Posix requires that geteuid and getegid are always successful. |
317 | 3 | let uid = unsafe { libc::geteuid() }; |
318 | | // SAFETY: As above. |
319 | 3 | let gid = unsafe { libc::getegid() }; |
320 | 3 | let uid_map = format!("{uid} {uid} 1\n"); |
321 | 3 | let gid_map = format!("{gid} {gid} 1\n"); |
322 | | // SAFETY: We ensure that if pid == 0 we only call async-signal-safe functions. |
323 | 3 | let pid = unsafe { libc::fork() }; |
324 | 3 | match pid { |
325 | | 0 => { |
326 | | // The mount namespace, private, is what the fresh /proc needs, |
327 | | // so here it is required. |
328 | 0 | if let Err(code) = enter_probe_namespaces(true, uid_map.as_bytes(), gid_map.as_bytes()) |
329 | | { |
330 | 0 | exit(code); |
331 | 0 | } |
332 | | // A proc mount reflects the PID namespace of the process that |
333 | | // mounts it, and after the unshare only the next child is in |
334 | | // the new one, as an action is. |
335 | | // SAFETY: Only async-signal-safe calls follow in either process. |
336 | 0 | match unsafe { libc::fork() } { |
337 | 0 | 0 => match mount_proc() { |
338 | 0 | Ok(()) => exit(0), |
339 | 0 | Err(err) => { |
340 | 0 | let errno = err.raw_os_error().unwrap_or(libc::EIO); |
341 | 0 | exit(probe_exit_code(NamespaceErrorType::Mount, errno)); |
342 | | } |
343 | | }, |
344 | 0 | child if child > 0 => { |
345 | 0 | let mut status = 0; |
346 | | // SAFETY: The pid is valid and created by us and the status is our own stack. |
347 | 0 | while unsafe { libc::waitpid(child, &raw mut status, 0) } == -1 { |
348 | | // SAFETY: We just called a libc function that failed (-1). |
349 | 0 | if unsafe { *libc::__errno_location() } != libc::EINTR { |
350 | 0 | exit(PROBE_FORK_FAILED); |
351 | 0 | } |
352 | | } |
353 | 0 | if libc::WIFEXITED(status) { |
354 | 0 | exit(libc::WEXITSTATUS(status)); |
355 | 0 | } |
356 | 0 | exit(PROBE_FORK_FAILED); |
357 | | } |
358 | 0 | _ => exit(PROBE_FORK_FAILED), |
359 | | } |
360 | | } |
361 | 3 | pid if pid > 0 => match wait_probe(pid) { |
362 | 3 | Some(0) => true, |
363 | | Some(PROBE_FORK_FAILED) => { |
364 | 0 | warn!("Namespaces: the fresh /proc probe could not fork"); |
365 | 0 | false |
366 | | } |
367 | 0 | Some(s) => { |
368 | 0 | warn!( |
369 | 0 | errno = s >> NS_ERROR_TYPE_BITS, |
370 | | "Namespaces: a fresh /proc cannot be mounted inside an action's PID namespace here, so actions see the host's /proc, which lists processes by their host PIDs" |
371 | | ); |
372 | 0 | false |
373 | | } |
374 | 0 | None => false, |
375 | | }, |
376 | 0 | _ => false, |
377 | | } |
378 | 3 | } |
379 | | |
380 | | /// Writes to a file in an async-signal-safe manner, does the write in a |
381 | | /// single chunk and assumes it will all be consumed, if the whole chunk |
382 | | /// is not written returns Err(EIO). This is expected to be used for |
383 | | /// special files such as /proc which will always accept the whole buffer. |
384 | 0 | fn write_signal_safe(file_name: &core::ffi::CStr, data: &[u8]) -> Result<(), core::ffi::c_int> { |
385 | | // SAFETY: The path is a CStr which is guaranteed to end in a NUL byte |
386 | | // and the returned file descriptor is always closed. |
387 | 0 | let fd = unsafe { libc::open(file_name.as_ptr().cast(), libc::O_WRONLY) }; |
388 | 0 | if fd < 0 { |
389 | | // SAFETY: We just called a libc function that failed (-1). |
390 | 0 | return Err(unsafe { *libc::__errno_location() }); |
391 | 0 | } |
392 | 0 | let fd = OwnedFd(fd); |
393 | | |
394 | | // SAFETY: The data is a known length slice and the file descriptor is |
395 | | // known to be valid as we just opened it. |
396 | 0 | let bytes_written = unsafe { libc::write(fd.0, data.as_ptr().cast(), data.len()) }; |
397 | | |
398 | 0 | if bytes_written == -1 { |
399 | | // SAFETY: We just called a libc function that failed (-1). |
400 | 0 | Err(unsafe { *libc::__errno_location() }) |
401 | 0 | } else if bytes_written as usize != data.len() { |
402 | 0 | Err(libc::EIO) |
403 | | } else { |
404 | 0 | Ok(()) |
405 | | } |
406 | 0 | } |
407 | | |
408 | | /// An async-signal-safe method to close all open file descriptors for the |
409 | | /// current process. This function is unsafe as any existing handles to |
410 | | /// file descriptors will be invalidated. None may be used after calling |
411 | | /// this function. |
412 | 0 | unsafe fn close_all_fds() { |
413 | | // SAFETY: It is safe to call close on all file descriptors as this is |
414 | | // the purpose of the function. |
415 | 0 | if unsafe { libc::syscall(libc::SYS_close_range, 0, libc::INT_MAX, 0) } == 0 { |
416 | 0 | return; |
417 | 0 | } |
418 | | // Since we're <5.9 kernel, we need to get the max FD count. |
419 | 0 | let mut rlim = core::mem::MaybeUninit::<libc::rlimit>::uninit(); |
420 | | // SAFETY: We just allocated the memory for this and getrlimit is async-signal-safe. |
421 | 0 | let max_fd = if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, rlim.as_mut_ptr()) } == 0 { |
422 | | // SAFETY: We just initialised this in getrlimit above that succeeded. |
423 | 0 | let cur = unsafe { rlim.assume_init().rlim_cur }; |
424 | 0 | if cur == libc::RLIM_INFINITY { |
425 | | // Sane fallback for unlimited environments |
426 | 0 | 0x0001_0000 |
427 | | } else { |
428 | 0 | core::ffi::c_int::try_from(cur).unwrap_or(0x0001_0000) |
429 | | } |
430 | | } else { |
431 | | // Fallback for getrlimit failure. |
432 | 0 | 4096 |
433 | | }; |
434 | 0 | for fd in 0..max_fd { |
435 | 0 | // SAFETY: It is safe to close a file descriptor that is not open and |
436 | 0 | // we also want to close all, so there's no issue with closing file |
437 | 0 | // descriptors that others may have handles to. |
438 | 0 | unsafe { libc::close(fd) }; |
439 | 0 | } |
440 | 0 | } |
441 | | |
442 | | /// Write the value n to the given slice as a decimal string. |
443 | 0 | fn u32_to_bytes(mut n: u32, buf: &mut [u8]) -> usize { |
444 | 0 | if n == 0 { |
445 | 0 | buf[0] = b'0'; |
446 | 0 | return 1; |
447 | 0 | } |
448 | 0 | let mut i = 0; |
449 | 0 | while n > 0 { |
450 | 0 | buf[i] = b'0' + (n % 10) as u8; |
451 | 0 | n /= 10; |
452 | 0 | i += 1; |
453 | 0 | } |
454 | 0 | buf[..i].reverse(); |
455 | 0 | i |
456 | 0 | } |
457 | | |
458 | | /// Create a line in the buffer of the format "{id} {id} 1\n" in an |
459 | | /// async-signal-safe manner. |
460 | 0 | fn create_map_line(id: u32, buffer: &mut [u8; 32]) -> &'_ [u8] { |
461 | 0 | let mut pos = 0; |
462 | 0 | pos += u32_to_bytes(id, &mut buffer[pos..]); |
463 | 0 | buffer[pos] = b' '; |
464 | 0 | pos += 1; |
465 | 0 | pos += u32_to_bytes(id, &mut buffer[pos..]); |
466 | 0 | buffer[pos] = b' '; |
467 | 0 | pos += 1; |
468 | 0 | buffer[pos] = b'1'; |
469 | 0 | pos += 1; |
470 | 0 | buffer[pos] = b'\n'; |
471 | 0 | pos += 1; |
472 | 0 | &buffer[..pos] |
473 | 0 | } |
474 | | |
475 | | /// A simple wrapper around a file descriptor to ensure async-signal-safety |
476 | | /// rather than the std version which may allocate. |
477 | | struct OwnedFd(libc::c_int); |
478 | | |
479 | | impl Drop for OwnedFd { |
480 | 0 | fn drop(&mut self) { |
481 | | // SAFETY: We own the file descriptor, so we can close it. |
482 | 0 | unsafe { |
483 | 0 | libc::close(self.0); |
484 | 0 | } |
485 | 0 | } |
486 | | } |
487 | | |
488 | | /// With a private `/tmp`, the tmpfs that masks the root action directory |
489 | | /// holds one directory entry and a mount point, so it is bounded to this: |
490 | | /// anything that writes beside the action directory instead of inside it |
491 | | /// gets `ENOSPC` rather than the worker's memory. Without `isolate_tmp` |
492 | | /// the mask is unbounded, as it was before the private `/tmp` existed. |
493 | | const ROOT_MASK_TMPFS_OPTIONS: &core::ffi::CStr = c"size=1m"; |
494 | | |
495 | | /// Whether `path` is `/tmp` or lies under it. |
496 | 0 | fn is_under_tmp(path: &core::ffi::CStr) -> bool { |
497 | 0 | let bytes = path.to_bytes(); |
498 | 0 | bytes == b"/tmp" || bytes.starts_with(b"/tmp/") |
499 | 0 | } |
500 | | |
501 | | /// Mount a fresh, empty tmpfs over the given directory in an |
502 | | /// async-signal-safe manner, with the given mount options, or none. |
503 | 0 | fn mount_tmpfs(target: &core::ffi::CStr, options: Option<&core::ffi::CStr>) -> Result<(), Error> { |
504 | | // SAFETY: mount is async-signal-safe. The filesystem type, target and options are valid C-strings. |
505 | | if unsafe { |
506 | 0 | libc::mount( |
507 | 0 | c"tmpfs".as_ptr(), |
508 | 0 | target.as_ptr(), |
509 | 0 | c"tmpfs".as_ptr(), |
510 | | 0, |
511 | 0 | options.map_or(core::ptr::null(), |options| options.as_ptr().cast()), |
512 | | ) |
513 | | } != 0 |
514 | | { |
515 | 0 | return Err(Error::last_os_error()); |
516 | 0 | } |
517 | 0 | Ok(()) |
518 | 0 | } |
519 | | |
520 | | /// Linux statvfs flag, not exposed by the libc crate on musl. |
521 | | const ST_RELATIME: libc::c_ulong = 0x1000; |
522 | | |
523 | | /// Make every mount in the current mount namespace private, so nothing |
524 | | /// mounted in it propagates back to the host. Async-signal-safe. |
525 | 0 | fn make_mounts_private() -> Result<(), Error> { |
526 | | // SAFETY: mount is async-signal-safe. We pass a null pointer for the source and valid |
527 | | // C-string pointers for the target. The parameters match POSIX requirements. |
528 | 0 | if unsafe { |
529 | 0 | libc::mount( |
530 | 0 | core::ptr::null(), |
531 | 0 | c"/".as_ptr(), |
532 | 0 | core::ptr::null(), |
533 | 0 | libc::MS_REC | libc::MS_PRIVATE, |
534 | 0 | core::ptr::null(), |
535 | 0 | ) |
536 | 0 | } != 0 |
537 | | { |
538 | 0 | return Err(Error::last_os_error()); |
539 | 0 | } |
540 | 0 | Ok(()) |
541 | 0 | } |
542 | | |
543 | | /// The flags for the fresh `proc` mounted over `/proc`: the usual |
544 | | /// restrictions, plus the read-only and atime flags the host's `/proc` |
545 | | /// carries. Inside a user namespace every inherited mount is locked, and |
546 | | /// the kernel refuses a new `proc` that would be more permissive than the |
547 | | /// locked one it covers, so those flags are mirrored rather than assumed. |
548 | | /// Async-signal-safe: the kernel reports the flags with the `statfs` call |
549 | | /// itself (`ST_VALID`, since 2.6.36), so neither libc reads anything else. |
550 | 0 | fn proc_mount_flags() -> libc::c_ulong { |
551 | 0 | let mut flags = libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC; |
552 | 0 | let mut stats = core::mem::MaybeUninit::<libc::statvfs>::uninit(); |
553 | | // SAFETY: the path is a valid C string and stats is writable storage. |
554 | 0 | if unsafe { libc::statvfs(c"/proc".as_ptr(), stats.as_mut_ptr()) } != 0 { |
555 | 0 | return flags; |
556 | 0 | } |
557 | | // SAFETY: statvfs initialized the structure on success. |
558 | 0 | let host = unsafe { stats.assume_init() }.f_flag; |
559 | 0 | for (stat_flag, mount_flag) in [ |
560 | 0 | (libc::ST_RDONLY, libc::MS_RDONLY), |
561 | 0 | (libc::ST_NOATIME, libc::MS_NOATIME), |
562 | 0 | (libc::ST_NODIRATIME, libc::MS_NODIRATIME), |
563 | 0 | (ST_RELATIME, libc::MS_RELATIME), |
564 | 0 | ] { |
565 | 0 | if host & stat_flag != 0 { |
566 | 0 | flags |= mount_flag; |
567 | 0 | } |
568 | | } |
569 | 0 | flags |
570 | 0 | } |
571 | | |
572 | | /// Mount a fresh `proc` over `/proc`. It reflects the PID namespace of the |
573 | | /// calling process, so call it from inside the action's. Async-signal-safe. |
574 | 0 | fn mount_proc() -> Result<(), Error> { |
575 | | // SAFETY: mount is async-signal-safe. The source, target and filesystem type are valid C-strings. |
576 | 0 | if unsafe { |
577 | 0 | libc::mount( |
578 | 0 | c"proc".as_ptr(), |
579 | 0 | c"/proc".as_ptr(), |
580 | 0 | c"proc".as_ptr(), |
581 | 0 | proc_mount_flags(), |
582 | 0 | core::ptr::null(), |
583 | 0 | ) |
584 | 0 | } != 0 |
585 | | { |
586 | 0 | return Err(Error::last_os_error()); |
587 | 0 | } |
588 | 0 | Ok(()) |
589 | 0 | } |
590 | | |
591 | | /// Bind mount `source` over `target` in an async-signal-safe manner. |
592 | 0 | fn bind_mount(source: &core::ffi::CStr, target: &core::ffi::CStr) -> Result<(), Error> { |
593 | | // SAFETY: mount is async-signal-safe. Both paths are valid C-strings. |
594 | 0 | if unsafe { |
595 | 0 | libc::mount( |
596 | 0 | source.as_ptr(), |
597 | 0 | target.as_ptr(), |
598 | 0 | core::ptr::null(), |
599 | 0 | libc::MS_BIND | libc::MS_REC, |
600 | 0 | core::ptr::null(), |
601 | 0 | ) |
602 | 0 | } != 0 |
603 | | { |
604 | 0 | return Err(Error::last_os_error()); |
605 | 0 | } |
606 | 0 | Ok(()) |
607 | 0 | } |
608 | | |
609 | | /// Create a directory and any missing parents in an async-signal-safe |
610 | | /// manner, like `mkdir -p`. Components that already exist are skipped, so |
611 | | /// this is a no-op for a path that is already present. |
612 | 0 | fn mkdir_p_signal_safe(path: &core::ffi::CStr) -> Result<(), Error> { |
613 | 0 | let bytes = path.to_bytes(); |
614 | | // Leave room for the NUL that terminates each prefix. |
615 | 0 | let mut buffer = [0u8; libc::PATH_MAX as usize]; |
616 | 0 | if bytes.len() >= buffer.len() { |
617 | 0 | return Err(Error::from_raw_os_error(libc::ENAMETOOLONG)); |
618 | 0 | } |
619 | 0 | buffer[..bytes.len()].copy_from_slice(bytes); |
620 | | // Start at 1 so a leading '/' is never treated as an empty component. |
621 | 0 | for i in 1..=bytes.len() { |
622 | 0 | if i < bytes.len() && buffer[i] != b'/' { |
623 | 0 | continue; |
624 | 0 | } |
625 | | // Temporarily terminate the path here so just this prefix is created. |
626 | 0 | buffer[i] = 0; |
627 | | // SAFETY: mkdir is async-signal-safe and the buffer is NUL-terminated at i. |
628 | 0 | if unsafe { libc::mkdir(buffer.as_ptr().cast(), 0o777) } != 0 { |
629 | 0 | let err = Error::last_os_error(); |
630 | 0 | if err.raw_os_error() != Some(libc::EEXIST) { |
631 | 0 | return Err(err); |
632 | 0 | } |
633 | 0 | } |
634 | 0 | if i < bytes.len() { |
635 | 0 | buffer[i] = b'/'; |
636 | 0 | } |
637 | | } |
638 | 0 | Ok(()) |
639 | 0 | } |
640 | | |
641 | 0 | fn perform_remount( |
642 | 0 | tmp_directory: Option<&core::ffi::CStr>, |
643 | 0 | root_action_directory: &core::ffi::CStr, |
644 | 0 | action_directory: &core::ffi::CStr, |
645 | 0 | input_mounts: &[ReadOnlyBindMount], |
646 | 0 | ) -> Result<(), Error> { |
647 | | // The caller has already made the mount namespace private, so nothing |
648 | | // below propagates back to the host. |
649 | | |
650 | | // Install inputs while cache paths are still visible. The recursive action |
651 | | // bind below preserves these mounts when its parent is masked or hidden by |
652 | | // the private /tmp. Resolving sources here also keeps them in this mount |
653 | | // namespace, unlike descriptors opened by the parent before unshare. |
654 | 0 | mount_readonly_inputs(input_mounts)?; |
655 | | |
656 | | // Bind mount the action directory to itself to "save" its current contents before |
657 | | // we mask its parent. |
658 | | // SAFETY: mount is async-signal-safe. We pass valid C-string pointers for the paths. |
659 | 0 | if unsafe { |
660 | 0 | libc::mount( |
661 | 0 | action_directory.as_ptr(), |
662 | 0 | action_directory.as_ptr(), |
663 | 0 | core::ptr::null(), |
664 | 0 | libc::MS_BIND | libc::MS_REC, |
665 | 0 | core::ptr::null(), |
666 | 0 | ) |
667 | 0 | } != 0 |
668 | | { |
669 | 0 | return Err(Error::last_os_error()); |
670 | 0 | } |
671 | | |
672 | | // Open the directory with O_PATH so we can find it after masking the parent. |
673 | | // SAFETY: open is async-signal-safe. The path is a valid C-string. |
674 | 0 | let fd = unsafe { libc::open(action_directory.as_ptr(), libc::O_PATH) }; |
675 | 0 | if fd < 0 { |
676 | 0 | return Err(Error::last_os_error()); |
677 | 0 | } |
678 | 0 | let fd = OwnedFd(fd); |
679 | | |
680 | 0 | if let Some(tmp_directory) = tmp_directory { |
681 | | // Give the action a private /tmp: its own tmp directory, on the |
682 | | // worker's disk and removed with the action, bound over /tmp (the |
683 | | // bind carries the work volume's mount options, noexec and the |
684 | | // like, not the host /tmp's; the config doc says so) so |
685 | | // concurrent actions cannot collide on predictable paths there and |
686 | | // nothing leaks between actions through it. A directory on disk |
687 | | // rather than a tmpfs, because tmpfs pages are memory charged to |
688 | | // the action's cgroup: a tool filling a memory-backed /tmp would |
689 | | // take the worker down with it, where filling a directory under |
690 | | // the scratch volume is bounded by that volume and the disk guard. |
691 | | // This has to happen before the root action directory is masked: if |
692 | | // that directory lives under /tmp the bind hides it, so its path is |
693 | | // recreated inside the bound directory and the action directory |
694 | | // itself is restored below from the saved file descriptor. When the |
695 | | // root action directory lives elsewhere every component already |
696 | | // exists and the mkdir is a no-op. |
697 | 0 | bind_mount(tmp_directory, c"/tmp")?; |
698 | 0 | mkdir_p_signal_safe(root_action_directory)?; |
699 | 0 | } |
700 | | |
701 | | // Mask the root action directory with a tmpfs to ensure sibling |
702 | | // directories aren't visible. With the root at or under a bound /tmp |
703 | | // the bind already hides every sibling, and a mask there would either |
704 | | // land on top of the bind (root exactly /tmp: the action's "private |
705 | | // /tmp" would be the 1 MiB mask) or leave a mount point sitting in the |
706 | | // action's otherwise empty /tmp, so it is skipped. |
707 | 0 | let root_under_bound_tmp = tmp_directory.is_some() && is_under_tmp(root_action_directory); |
708 | 0 | if !root_under_bound_tmp { |
709 | 0 | let options = tmp_directory.is_some().then_some(ROOT_MASK_TMPFS_OPTIONS); |
710 | 0 | mount_tmpfs(root_action_directory, options)?; |
711 | 0 | } |
712 | | |
713 | | // Recreate the specific operation's directory inside the empty tmpfs. |
714 | | // SAFETY: mkdir is async-signal-safe and the path is a valid C-string. |
715 | 0 | if unsafe { libc::mkdir(action_directory.as_ptr(), 0o777) } != 0 { |
716 | 0 | return Err(Error::last_os_error()); |
717 | 0 | } |
718 | | |
719 | | // Bind mount the saved directory back from the file descriptor to the new path. |
720 | 0 | let mut proc_path = [0u8; 64]; |
721 | 0 | let mut pos = 0; |
722 | 0 | for &b in b"/proc/self/fd/" { |
723 | 0 | proc_path[pos] = b; |
724 | 0 | pos += 1; |
725 | 0 | } |
726 | 0 | pos += u32_to_bytes(fd.0 as u32, &mut proc_path[pos..]); |
727 | 0 | proc_path[pos] = 0; |
728 | | |
729 | | // SAFETY: mount is async-signal-safe. The target path is a valid C-string and the source |
730 | | // path is correctly formatted using /proc/self/fd/. |
731 | 0 | if unsafe { |
732 | 0 | libc::mount( |
733 | 0 | proc_path.as_ptr().cast(), |
734 | 0 | action_directory.as_ptr(), |
735 | 0 | core::ptr::null(), |
736 | 0 | libc::MS_BIND | libc::MS_REC, |
737 | 0 | core::ptr::null(), |
738 | 0 | ) |
739 | 0 | } != 0 |
740 | | { |
741 | 0 | return Err(Error::last_os_error()); |
742 | 0 | } |
743 | | |
744 | 0 | Ok(()) |
745 | 0 | } |
746 | | |
747 | | /// Paths for a bind mount, allocated in the parent before the pre-exec hook. |
748 | | /// The caller must pin the cache entry until the action exits and its mounts are released. |
749 | | #[derive(Debug)] |
750 | | pub struct ReadOnlyBindMount { |
751 | | source: std::ffi::CString, |
752 | | target: std::ffi::CString, |
753 | | remount_flags: libc::c_ulong, |
754 | | } |
755 | | |
756 | | impl ReadOnlyBindMount { |
757 | 7 | pub fn new(source: &std::path::Path, target: &std::path::Path) -> Result<Self, Error> { |
758 | | use std::os::unix::ffi::OsStrExt; |
759 | | |
760 | | // The child changes its working directory before running pre-exec. |
761 | 7 | let source = std::path::absolute(source)?0 ; |
762 | 7 | let target = std::path::absolute(target)?0 ; |
763 | 7 | let source = std::ffi::CString::new(source.as_os_str().as_bytes())?0 ; |
764 | 7 | let mut stats = core::mem::MaybeUninit::<libc::statvfs>::uninit(); |
765 | | // SAFETY: the source is a valid C string and stats is writable storage. |
766 | 7 | if unsafe { libc::statvfs(source.as_ptr(), stats.as_mut_ptr()) } != 0 { |
767 | 0 | return Err(Error::last_os_error()); |
768 | 7 | } |
769 | | // SAFETY: statvfs initialized the structure on success. |
770 | 7 | let flags = unsafe { stats.assume_init() }.f_flag; |
771 | 7 | let mut remount_flags = |
772 | 7 | libc::MS_BIND | libc::MS_REMOUNT | libc::MS_RDONLY | libc::MS_NOSUID | libc::MS_NODEV; |
773 | | // A user namespace cannot clear inherited locked mount flags. Preserve |
774 | | // execute and atime restrictions while adding read-only protection. |
775 | 28 | for (stat_flag, mount_flag) in [ |
776 | 7 | (libc::ST_NOEXEC, libc::MS_NOEXEC), |
777 | 7 | (libc::ST_NOATIME, libc::MS_NOATIME), |
778 | 7 | (libc::ST_NODIRATIME, libc::MS_NODIRATIME), |
779 | 7 | (ST_RELATIME, libc::MS_RELATIME), |
780 | 7 | ] { |
781 | 28 | if flags & stat_flag != 0 { |
782 | 7 | remount_flags |= mount_flag; |
783 | 21 | } |
784 | | } |
785 | | Ok(Self { |
786 | 7 | source, |
787 | 7 | target: std::ffi::CString::new(target.as_os_str().as_bytes())?0 , |
788 | 7 | remount_flags, |
789 | | }) |
790 | 7 | } |
791 | | } |
792 | | |
793 | | /// Installs prepared input trees inside the private mount namespace, before |
794 | | /// masking the action root. |
795 | | /// |
796 | | /// Call only from the child's pre-exec hook: all strings and flags are |
797 | | /// prepared beforehand, and this function uses only mount syscalls and errno. |
798 | | /// Failure aborts the spawn rather than executing against empty placeholders. |
799 | 0 | fn mount_readonly_inputs(mounts: &[ReadOnlyBindMount]) -> Result<(), Error> { |
800 | 0 | for mount in mounts { |
801 | | // SAFETY: both paths are valid, preallocated C strings; the source |
802 | | // cache entry is kept alive by the action through its cleanup. |
803 | 0 | if unsafe { |
804 | 0 | libc::mount( |
805 | 0 | mount.source.as_ptr(), |
806 | 0 | mount.target.as_ptr(), |
807 | 0 | core::ptr::null(), |
808 | 0 | libc::MS_BIND, |
809 | 0 | core::ptr::null(), |
810 | 0 | ) |
811 | 0 | } != 0 |
812 | | { |
813 | 0 | return Err(Error::last_os_error()); |
814 | 0 | } |
815 | | // MS_RDONLY on the initial bind is ignored by Linux; remount the |
816 | | // action's bind explicitly. This leaves the worker's cache mount alone. |
817 | | // SAFETY: this modifies only the bind in the child's private namespace. |
818 | 0 | if unsafe { |
819 | 0 | libc::mount( |
820 | 0 | core::ptr::null(), |
821 | 0 | mount.target.as_ptr(), |
822 | 0 | core::ptr::null(), |
823 | 0 | mount.remount_flags, |
824 | 0 | core::ptr::null(), |
825 | 0 | ) |
826 | 0 | } != 0 |
827 | | { |
828 | 0 | return Err(Error::last_os_error()); |
829 | 0 | } |
830 | | } |
831 | 0 | Ok(()) |
832 | 0 | } |
833 | | |
834 | | /// A hook for a `Command::spawn` to create the process in a new namespace. |
835 | | /// This creates a stub process that the Command points at which forwards |
836 | | /// SIGKILL to the actual process in the new user, PID, UTS, IPC and mount |
837 | | /// namespaces. Pass this function to `CommandBuilder::pre_exec`. |
838 | | /// |
839 | | /// The process is PID 1 of its PID namespace and gets a fresh `proc` over |
840 | | /// `/proc`, so what it finds there is its own processes under the PIDs it |
841 | | /// knows them by. The mount namespace is private, so that mount never |
842 | | /// reaches the host; where the host refuses it (see |
843 | | /// [`fresh_proc_supported`]) the process keeps the host's `/proc`. |
844 | | /// |
845 | | /// When `mount` is set the root action directory is masked in that mount |
846 | | /// namespace so sibling actions are not visible. When `tmp_directory` is |
847 | | /// given as well, that directory (the action's own, already created) is |
848 | | /// bound over `/tmp`, so the action has a private `/tmp` that is removed |
849 | | /// with it. |
850 | | /// |
851 | | /// This function is async-signal-safe and has no external locks or |
852 | | /// memory allocations. |
853 | 0 | pub fn configure_namespace( |
854 | 0 | mount: bool, |
855 | 0 | tmp_directory: Option<&core::ffi::CStr>, |
856 | 0 | root_action_directory: &core::ffi::CStr, |
857 | 0 | action_directory: &core::ffi::CStr, |
858 | 0 | ) -> std::io::Result<()> { |
859 | 0 | configure_namespace_with_input_mounts( |
860 | 0 | mount, |
861 | 0 | tmp_directory, |
862 | 0 | root_action_directory, |
863 | 0 | action_directory, |
864 | 0 | &[], |
865 | | ) |
866 | 0 | } |
867 | | |
868 | | /// Like `configure_namespace`, with prepared immutable input trees. Mounts are |
869 | | /// installed only after making the namespace private, and before binding the |
870 | | /// private `/tmp` and masking the action root, so sources under either stay |
871 | | /// reachable. All paths must be allocated and cache entries pinned by the |
872 | | /// parent. This function is async-signal-safe. |
873 | 0 | pub fn configure_namespace_with_input_mounts( |
874 | 0 | mount: bool, |
875 | 0 | tmp_directory: Option<&core::ffi::CStr>, |
876 | 0 | root_action_directory: &core::ffi::CStr, |
877 | 0 | action_directory: &core::ffi::CStr, |
878 | 0 | input_mounts: &[ReadOnlyBindMount], |
879 | 0 | ) -> std::io::Result<()> { |
880 | 0 | if !mount && !input_mounts.is_empty() { |
881 | 0 | return Err(Error::from_raw_os_error(libc::EINVAL)); |
882 | 0 | } |
883 | | // SAFETY: It is always safe to call geteuid on Posix. |
884 | 0 | let uid = unsafe { libc::geteuid() }; |
885 | | // SAFETY: It is always safe to call getegid on Posix. |
886 | 0 | let gid = unsafe { libc::getegid() }; |
887 | | |
888 | 0 | let has_mount_namespace = unshare_action_namespaces(mount).map_err(Error::from_raw_os_error)?; |
889 | | |
890 | 0 | if let Err(e) = write_signal_safe(c"/proc/self/setgroups", b"deny") { |
891 | | // If we fail to write this it will just make gid_map fail later, |
892 | | // but we may be able to continue anyway. |
893 | 0 | if e != libc::EPERM && e != libc::EACCES && e != libc::ENOENT { |
894 | 0 | return Err(Error::from_raw_os_error(e)); |
895 | 0 | } |
896 | 0 | } |
897 | | |
898 | 0 | let mut buffer = [0u8; 32]; |
899 | 0 | write_signal_safe(c"/proc/self/uid_map", create_map_line(uid, &mut buffer)) |
900 | 0 | .map_err(Error::from_raw_os_error)?; |
901 | | |
902 | | // If we can't write to gid_map, we just ignore it. This usually happens if |
903 | | // setgroups was not written to (because of permissions) or if we are in a |
904 | | // restricted environment. |
905 | 0 | if let Err(e) = write_signal_safe(c"/proc/self/gid_map", create_map_line(gid, &mut buffer)) { |
906 | | // If this fails then we can probably continue just fine, it's just |
907 | | // the uid that's important. |
908 | 0 | if e != libc::EPERM && e != libc::EACCES { |
909 | 0 | return Err(Error::from_raw_os_error(e)); |
910 | 0 | } |
911 | 0 | } |
912 | | |
913 | | // Nothing mounted from here on, the fresh /proc included, may reach |
914 | | // the host. Without a private mount namespace there is no fresh /proc. |
915 | 0 | let fresh_proc = has_mount_namespace && privatize_mounts(mount)?; |
916 | | |
917 | | // Mask the root action directory if enabled. |
918 | 0 | if mount { |
919 | 0 | perform_remount( |
920 | 0 | tmp_directory, |
921 | 0 | root_action_directory, |
922 | 0 | action_directory, |
923 | 0 | input_mounts, |
924 | 0 | )?; |
925 | 0 | } |
926 | | |
927 | | // Set hostname to "nativelink" to ensure reproducibility. |
928 | 0 | let hostname = b"nativelink"; |
929 | | // SAFETY: We reference the static memory above only and this is |
930 | | // async-signal-safe. |
931 | 0 | if unsafe { libc::sethostname(hostname.as_ptr().cast(), hostname.len()) } != 0 { |
932 | | // SAFETY: We just called a libc function that failed. |
933 | 0 | let err = unsafe { *libc::__errno_location() }; |
934 | 0 | if err != libc::EPERM && err != libc::EACCES { |
935 | 0 | return Err(Error::from_raw_os_error(err)); |
936 | 0 | } |
937 | 0 | } |
938 | | |
939 | | // Fork to enter the PID namespace. |
940 | | // SAFETY: We are already in a required async-signal-safe environment, we |
941 | | // will continue to ensure that ongoing. |
942 | 0 | match unsafe { libc::fork() } { |
943 | | 0 => { |
944 | | // SAFETY: This function is async-signal-safe and references no memory or resources. |
945 | 0 | if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) } != 0 { |
946 | 0 | exit(1); |
947 | 0 | } |
948 | | // This process is PID 1 of the action's PID namespace, so a |
949 | | // proc it mounts lists that namespace: a child the action forks |
950 | | // is found under /proc by the PID the fork returned, not |
951 | | // whichever host process has that number. A refusal is the one |
952 | | // `fresh_proc_supported` warned about at startup, and leaves the |
953 | | // host's /proc in place as before. |
954 | 0 | if fresh_proc |
955 | 0 | && let Err(err) = mount_proc() |
956 | 0 | && !err.raw_os_error().is_some_and(is_denial) |
957 | | { |
958 | 0 | return Err(err); |
959 | 0 | } |
960 | 0 | Ok(()) |
961 | | } |
962 | 0 | pid if pid > 0 => { |
963 | | // Ensure that any children spawned by the action are re-parented to |
964 | | // this process if their parent dies. This is effectively a sub-reaper. |
965 | | // SAFETY: prctl is async-signal-safe. |
966 | 0 | unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) }; |
967 | | |
968 | | // SAFETY: All operations below simply _exit and therefore there |
969 | | // are no issues with dangling file descriptor handles. |
970 | 0 | unsafe { close_all_fds() }; |
971 | | |
972 | 0 | let mut sigset = core::mem::MaybeUninit::<libc::sigset_t>::uninit(); |
973 | | // SAFETY: sigset is on the stack and we are initializing it. |
974 | 0 | unsafe { |
975 | 0 | libc::sigemptyset(sigset.as_mut_ptr()); |
976 | 0 | libc::sigaddset(sigset.as_mut_ptr(), libc::SIGTERM); |
977 | 0 | libc::sigaddset(sigset.as_mut_ptr(), libc::SIGCHLD); |
978 | 0 | libc::sigprocmask(libc::SIG_BLOCK, sigset.as_ptr(), core::ptr::null_mut()); |
979 | 0 | } |
980 | | |
981 | | loop { |
982 | | // Reap all exited children. |
983 | | loop { |
984 | 0 | let mut status = 0; |
985 | | // SAFETY: The status is on the stack and waitpid is otherwise |
986 | | // safe to call. |
987 | 0 | let res = unsafe { libc::waitpid(-1, &raw mut status, libc::WNOHANG) }; |
988 | 0 | if res == pid { |
989 | 0 | if libc::WIFEXITED(status) { |
990 | 0 | exit(libc::WEXITSTATUS(status)); |
991 | 0 | } else if libc::WIFSIGNALED(status) { |
992 | | // Try to exit with the same signal as the child. |
993 | | // SAFETY: The sigset was previously allocated and used on the stack. |
994 | | unsafe { |
995 | 0 | libc::sigprocmask( |
996 | | libc::SIG_UNBLOCK, |
997 | 0 | sigset.as_ptr(), |
998 | 0 | core::ptr::null_mut(), |
999 | | ) |
1000 | | }; |
1001 | | // SAFETY: It's always safe to raise and as a fallback we _exit below. |
1002 | 0 | unsafe { libc::raise(libc::WTERMSIG(status)) }; |
1003 | | // We shouldn't get here, but it's a fallback in case. |
1004 | 0 | exit(libc::WTERMSIG(status)); |
1005 | 0 | } |
1006 | 0 | } else if res <= 0 { |
1007 | | // SAFETY: We just called a libc function that failed. |
1008 | 0 | if res == -1 && unsafe { *libc::__errno_location() } != libc::EINTR { |
1009 | 0 | exit(255); |
1010 | 0 | } |
1011 | | // Break the reaping loop to wait for signals. |
1012 | 0 | break; |
1013 | 0 | } |
1014 | | } |
1015 | | |
1016 | 0 | let mut siginfo = core::mem::MaybeUninit::<libc::siginfo_t>::uninit(); |
1017 | | // SAFETY: sigset is initialized and siginfo is on the stack. |
1018 | 0 | let sig = unsafe { libc::sigwaitinfo(sigset.as_ptr(), siginfo.as_mut_ptr()) }; |
1019 | | |
1020 | 0 | if sig == libc::SIGTERM { |
1021 | 0 | // Pass the SIGTERM on, so the action gets the grace the |
1022 | 0 | // worker's kill_grace_ms promises to write its own |
1023 | 0 | // cleanup. The stub used to turn it into SIGKILL, which |
1024 | 0 | // ended the action at once under namespaces while the |
1025 | 0 | // same action got its grace without them. The hard kill |
1026 | 0 | // is a SIGKILL to the stub itself: as this PID |
1027 | 0 | // namespace's init, its death takes every process in |
1028 | 0 | // the namespace with it. |
1029 | 0 | // SAFETY: pid is valid and we are sending a signal. |
1030 | 0 | unsafe { libc::kill(pid, libc::SIGTERM) }; |
1031 | 0 | } |
1032 | | } |
1033 | | } |
1034 | 0 | _ => Err(Error::last_os_error()), |
1035 | | } |
1036 | 0 | } |