Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}