Coverage Report

Created: 2026-09-18 20:40

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 The NativeLink Authors. 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;
18
19
/// A wrapper around a Child to send SIGTERM to kill the process instead
20
/// of SIGKILL as it's wrapped by the stub.
21
#[derive(Debug)]
22
pub struct MaybeNamespacedChild {
23
    namespaced: bool,
24
    child: tokio::process::Child,
25
}
26
27
impl MaybeNamespacedChild {
28
23
    pub const fn new(namespaced: bool, child: tokio::process::Child) -> Self {
29
23
        Self { namespaced, child }
30
23
    }
31
32
18
    pub fn id(&self) -> Option<u32> {
33
18
        self.child.id()
34
18
    }
35
36
    /// Send SIGTERM if namespaced which sends SIGKILL to the child, otherwise
37
    /// send SIGKILL to the child.
38
7
    
pub async fn kill(&mut self) -> Result<(), Error>0
{
39
7
        if self.namespaced {
40
            // It would be safer to call send_signal to use the pidfd to avoid
41
            // races, however this is still an experimental API, see:
42
            // https://github.com/rust-lang/rust/issues/141975
43
            // self.child.std_child().send_signal(Signal::SIGTERM)?;
44
            // return self.child.wait().await.map(|_| ());
45
6
            if let Some(pid) = self.child.id() {
46
6
                let pid_t: libc::pid_t = pid.try_into().map_err(|e| 
{0
47
0
                    Error::new(
48
0
                        std::io::ErrorKind::InvalidInput,
49
0
                        format!("pid larger than pid_t type ({pid}): {e}"),
50
                    )
51
0
                })?;
52
                // SAFETY: pid is valid as provided by the wrapper and we are
53
                // sending a signal to the namespaced stub.
54
6
                unsafe { libc::kill(pid_t, libc::SIGTERM) };
55
6
                return self.child.wait().await.map(|_| ());
56
0
            }
57
1
        }
58
1
        self.child.kill().await
59
7
    }
60
61
2
    pub fn try_wait(&mut self) -> Result<Option<std::process::ExitStatus>, Error> {
62
2
        self.child.try_wait()
63
2
    }
64
65
25
    
pub async fn wait(&mut self) -> Result<std::process::ExitStatus, Error>0
{
66
25
        self.child.wait().await
67
22
    }
68
}
69
70
0
fn exit(status: i32) -> ! {
71
    // SAFETY: It is always safe to _exit.
72
0
    unsafe { libc::_exit(status) };
73
}
74
75
enum NamespaceErrorType {
76
    Unshare = 1,
77
    WriteSignalSafe,
78
    Mount,
79
}
80
81
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
82
const NS_ERROR_TYPE_MASK: i32 = 0x3; // 11 - i.e. NS_ERROR_TYPE_BITS lowest bits
83
84
/// Determines whether the namespaces provided by this module are supported
85
/// on the currently running system by forking a process and trying to enter
86
/// it into the new namespaces.
87
40
pub fn namespaces_supported(mount: bool) -> bool {
88
    // SAFETY: Posix requires that geteuid is always successful.
89
40
    let uid = unsafe { libc::geteuid() };
90
40
    let uid_map = format!("{uid} {uid} 1\n");
91
    // SAFETY: We ensure that if pid == 0 we only call async-signal-safe functions.
92
40
    let pid = unsafe { libc::fork() };
93
40
    match pid {
94
        0 => {
95
0
            let mut flags =
96
0
                libc::CLONE_NEWPID | libc::CLONE_NEWUSER | libc::CLONE_NEWIPC | libc::CLONE_NEWUTS;
97
0
            if mount {
98
0
                flags |= libc::CLONE_NEWNS;
99
0
            }
100
            // SAFETY: Unshare does not have any unsafe effects and modifies no
101
            // memory, it is also async-signal-safe.
102
0
            if unsafe { libc::unshare(flags) } == 0 {
103
0
                match write_signal_safe(c"/proc/self/uid_map", uid_map.as_bytes()) {
104
                    Ok(()) => {
105
0
                        if !mount {
106
0
                            exit(0);
107
0
                        }
108
                        // SAFETY: Mount uses no memory and is async-signal-safe.
109
0
                        if unsafe {
110
0
                            libc::mount(
111
0
                                core::ptr::null(),
112
0
                                c"/".as_ptr(),
113
0
                                core::ptr::null(),
114
0
                                libc::MS_REC | libc::MS_PRIVATE,
115
0
                                core::ptr::null(),
116
0
                            )
117
0
                        } == 0
118
                        {
119
0
                            exit(0);
120
0
                        }
121
                        // SAFETY: We just called a libc function that failed (-1).
122
0
                        let errno = unsafe { *libc::__errno_location() };
123
0
                        exit((NamespaceErrorType::Mount as i32) | (errno << NS_ERROR_TYPE_BITS));
124
                    }
125
0
                    Err(uid_map_err) => {
126
0
                        exit(
127
0
                            (NamespaceErrorType::WriteSignalSafe as i32)
128
0
                                | (uid_map_err << NS_ERROR_TYPE_BITS),
129
                        );
130
                    }
131
                }
132
0
            }
133
            // SAFETY: We just called a libc function that failed (-1).
134
0
            let errno = unsafe { *libc::__errno_location() };
135
0
            exit((NamespaceErrorType::Unshare as i32) | (errno << NS_ERROR_TYPE_BITS));
136
        }
137
40
        pid if pid > 0 => {
138
40
            let mut status = 0;
139
            // SAFETY: The pid is valid and created by us and the status is our own stack.
140
40
            while unsafe { libc::waitpid(pid, &raw mut status, 0) } == -1 {
141
                // SAFETY: We just called a libc function that failed (-1).
142
0
                let errno = unsafe { *libc::__errno_location() };
143
0
                if errno != libc::EINTR {
144
0
                    error!(errno = errno, "Namespaces: Failure in waitpid");
145
0
                    return false;
146
0
                }
147
            }
148
40
            if libc::WIFEXITED(status) {
149
40
                match libc::WEXITSTATUS(status) {
150
                    0 => {
151
40
                        return true;
152
                    }
153
0
                    s if s & NS_ERROR_TYPE_MASK == NamespaceErrorType::Unshare as i32 => {
154
0
                        let errno = s >> NS_ERROR_TYPE_BITS;
155
0
                        error!(errno, "Namespaces: Error during unshare");
156
0
                        if errno == libc::EPERM {
157
0
                            error!(
158
                                "If the worker is inside Docker, namespaces don't work unless it's a privileged container"
159
                            );
160
0
                        }
161
                    }
162
0
                    s if s & NS_ERROR_TYPE_MASK == NamespaceErrorType::WriteSignalSafe as i32 => {
163
0
                        error!(
164
0
                            errno = s >> NS_ERROR_TYPE_BITS,
165
                            "Namespaces: Error while writing to /proc/self/uid_map"
166
                        );
167
                    }
168
0
                    s if s & NS_ERROR_TYPE_MASK == NamespaceErrorType::Mount as i32 => {
169
0
                        error!(
170
0
                            errno = s >> NS_ERROR_TYPE_BITS,
171
                            "Failure to mount during namespace checking"
172
                        );
173
                    }
174
0
                    other => {
175
0
                        error!(
176
                            exit_code = other,
177
                            "Namespace check failure with unknown exit code"
178
                        );
179
                    }
180
                }
181
            } else {
182
0
                error!(
183
                    exit_code = status,
184
                    "Namespaces: waitpid exit with non-exit code"
185
                );
186
            }
187
0
            false
188
        }
189
0
        _ => false,
190
    }
191
40
}
192
193
/// Writes to a file in an async-signal-safe manner, does the write in a
194
/// single chunk and assumes it will all be consumed, if the whole chunk
195
/// is not written returns Err(EIO).  This is expected to be used for
196
/// special files such as /proc which will always accept the whole buffer.
197
0
fn write_signal_safe(file_name: &core::ffi::CStr, data: &[u8]) -> Result<(), core::ffi::c_int> {
198
    // SAFETY: The path is a CStr which is guaranteed to end in a NUL byte
199
    // and the returned file descriptor is always closed.
200
0
    let fd = unsafe { libc::open(file_name.as_ptr().cast(), libc::O_WRONLY) };
201
0
    if fd < 0 {
202
        // SAFETY: We just called a libc function that failed (-1).
203
0
        return Err(unsafe { *libc::__errno_location() });
204
0
    }
205
0
    let fd = OwnedFd(fd);
206
207
    // SAFETY: The data is a known length slice and the file descriptor is
208
    // known to be valid as we just opened it.
209
0
    let bytes_written = unsafe { libc::write(fd.0, data.as_ptr().cast(), data.len()) };
210
211
0
    if bytes_written == -1 {
212
        // SAFETY: We just called a libc function that failed (-1).
213
0
        Err(unsafe { *libc::__errno_location() })
214
0
    } else if bytes_written as usize != data.len() {
215
0
        Err(libc::EIO)
216
    } else {
217
0
        Ok(())
218
    }
219
0
}
220
221
/// An async-signal-safe method to close all open file descriptors for the
222
/// current process.  This function is unsafe as any existing handles to
223
/// file descriptors will be invalidated.  None may be used after calling
224
/// this function.
225
0
unsafe fn close_all_fds() {
226
    // SAFETY: It is safe to call close on all file descriptors as this is
227
    // the purpose of the function.
228
0
    if unsafe { libc::syscall(libc::SYS_close_range, 0, libc::INT_MAX, 0) } == 0 {
229
0
        return;
230
0
    }
231
    // Since we're <5.9 kernel, we need to get the max FD count.
232
0
    let mut rlim = core::mem::MaybeUninit::<libc::rlimit>::uninit();
233
    // SAFETY: We just allocated the memory for this and getrlimit is async-signal-safe.
234
0
    let max_fd = if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, rlim.as_mut_ptr()) } == 0 {
235
        // SAFETY: We just initialised this in getrlimit above that succeeded.
236
0
        let cur = unsafe { rlim.assume_init().rlim_cur };
237
0
        if cur == libc::RLIM_INFINITY {
238
            // Sane fallback for unlimited environments
239
0
            0x0001_0000
240
        } else {
241
0
            core::ffi::c_int::try_from(cur).unwrap_or(0x0001_0000)
242
        }
243
    } else {
244
        // Fallback for getrlimit failure.
245
0
        4096
246
    };
247
0
    for fd in 0..max_fd {
248
0
        // SAFETY: It is safe to close a file descriptor that is not open and
249
0
        // we also want to close all, so there's no issue with closing file
250
0
        // descriptors that others may have handles to.
251
0
        unsafe { libc::close(fd) };
252
0
    }
253
0
}
254
255
/// Write the value n to the given slice as a decimal string.
256
0
fn u32_to_bytes(mut n: u32, buf: &mut [u8]) -> usize {
257
0
    if n == 0 {
258
0
        buf[0] = b'0';
259
0
        return 1;
260
0
    }
261
0
    let mut i = 0;
262
0
    while n > 0 {
263
0
        buf[i] = b'0' + (n % 10) as u8;
264
0
        n /= 10;
265
0
        i += 1;
266
0
    }
267
0
    buf[..i].reverse();
268
0
    i
269
0
}
270
271
/// Create a line in the buffer of the format "{id} {id} 1\n" in an
272
/// async-signal-safe manner.
273
0
fn create_map_line(id: u32, buffer: &mut [u8; 32]) -> &'_ [u8] {
274
0
    let mut pos = 0;
275
0
    pos += u32_to_bytes(id, &mut buffer[pos..]);
276
0
    buffer[pos] = b' ';
277
0
    pos += 1;
278
0
    pos += u32_to_bytes(id, &mut buffer[pos..]);
279
0
    buffer[pos] = b' ';
280
0
    pos += 1;
281
0
    buffer[pos] = b'1';
282
0
    pos += 1;
283
0
    buffer[pos] = b'\n';
284
0
    pos += 1;
285
0
    &buffer[..pos]
286
0
}
287
288
/// A simple wrapper around a file descriptor to ensure async-signal-safety
289
/// rather than the std version which may allocate.
290
struct OwnedFd(libc::c_int);
291
292
impl Drop for OwnedFd {
293
0
    fn drop(&mut self) {
294
        // SAFETY: We own the file descriptor, so we can close it.
295
0
        unsafe {
296
0
            libc::close(self.0);
297
0
        }
298
0
    }
299
}
300
301
0
fn perform_remount(
302
0
    root_action_directory: &core::ffi::CStr,
303
0
    action_directory: &core::ffi::CStr,
304
0
) -> Result<(), Error> {
305
    // Make the mount namespace private to avoid changes propagating back to the host.
306
    // SAFETY: mount is async-signal-safe. We pass a null pointer for the source and valid
307
    // C-string pointers for the target. The parameters match POSIX requirements.
308
0
    if unsafe {
309
0
        libc::mount(
310
0
            core::ptr::null(),
311
0
            c"/".as_ptr(),
312
0
            core::ptr::null(),
313
0
            libc::MS_REC | libc::MS_PRIVATE,
314
0
            core::ptr::null(),
315
0
        )
316
0
    } != 0
317
    {
318
0
        return Err(Error::last_os_error());
319
0
    }
320
321
    // Bind mount the action directory to itself to "save" its current contents before
322
    // we mask its parent.
323
    // SAFETY: mount is async-signal-safe. We pass valid C-string pointers for the paths.
324
0
    if unsafe {
325
0
        libc::mount(
326
0
            action_directory.as_ptr(),
327
0
            action_directory.as_ptr(),
328
0
            core::ptr::null(),
329
0
            libc::MS_BIND | libc::MS_REC,
330
0
            core::ptr::null(),
331
0
        )
332
0
    } != 0
333
    {
334
0
        return Err(Error::last_os_error());
335
0
    }
336
337
    // Open the directory with O_PATH so we can find it after masking the parent.
338
    // SAFETY: open is async-signal-safe. The path is a valid C-string.
339
0
    let fd = unsafe { libc::open(action_directory.as_ptr(), libc::O_PATH) };
340
0
    if fd < 0 {
341
0
        return Err(Error::last_os_error());
342
0
    }
343
0
    let fd = OwnedFd(fd);
344
345
    // Mask the root action directory with a tmpfs to ensure sibling directories aren't visible.
346
    // SAFETY: mount is async-signal-safe. The filesystem type and target are valid C-strings.
347
0
    if unsafe {
348
0
        libc::mount(
349
0
            c"tmpfs".as_ptr(),
350
0
            root_action_directory.as_ptr(),
351
0
            c"tmpfs".as_ptr(),
352
0
            0,
353
0
            core::ptr::null(),
354
0
        )
355
0
    } != 0
356
    {
357
0
        return Err(Error::last_os_error());
358
0
    }
359
360
    // Recreate the specific operation's directory inside the empty tmpfs.
361
    // SAFETY: mkdir is async-signal-safe and the path is a valid C-string.
362
0
    if unsafe { libc::mkdir(action_directory.as_ptr(), 0o777) } != 0 {
363
0
        return Err(Error::last_os_error());
364
0
    }
365
366
    // Bind mount the saved directory back from the file descriptor to the new path.
367
0
    let mut proc_path = [0u8; 64];
368
0
    let mut pos = 0;
369
0
    for &b in b"/proc/self/fd/" {
370
0
        proc_path[pos] = b;
371
0
        pos += 1;
372
0
    }
373
0
    pos += u32_to_bytes(fd.0 as u32, &mut proc_path[pos..]);
374
0
    proc_path[pos] = 0;
375
376
    // SAFETY: mount is async-signal-safe. The target path is a valid C-string and the source
377
    // path is correctly formatted using /proc/self/fd/.
378
0
    if unsafe {
379
0
        libc::mount(
380
0
            proc_path.as_ptr().cast(),
381
0
            action_directory.as_ptr(),
382
0
            core::ptr::null(),
383
0
            libc::MS_BIND | libc::MS_REC,
384
0
            core::ptr::null(),
385
0
        )
386
0
    } != 0
387
    {
388
0
        return Err(Error::last_os_error());
389
0
    }
390
391
0
    Ok(())
392
0
}
393
394
/// A hook for a `Command::spawn` to create the process in a new namespace.
395
/// This creates a stub process that the Command points at which forwards
396
/// SIGKILL to the actual process in the new user, PID, UTS and IPC
397
/// namespaces.  Pass this function to `CommandBuilder::pre_exec`.
398
///
399
/// This function is async-signal-safe and has no external locks or
400
/// memory allocations.
401
0
pub fn configure_namespace(
402
0
    mount: bool,
403
0
    root_action_directory: &core::ffi::CStr,
404
0
    action_directory: &core::ffi::CStr,
405
0
) -> std::io::Result<()> {
406
    // SAFETY: It is always safe to call geteuid on Posix.
407
0
    let uid = unsafe { libc::geteuid() };
408
    // SAFETY: It is always safe to call getegid on Posix.
409
0
    let gid = unsafe { libc::getegid() };
410
411
0
    let mut flags =
412
0
        libc::CLONE_NEWPID | libc::CLONE_NEWUSER | libc::CLONE_NEWIPC | libc::CLONE_NEWUTS;
413
0
    if mount {
414
0
        flags |= libc::CLONE_NEWNS;
415
0
    }
416
    // SAFETY: Unshare does not have any unsafe effects and modifies no
417
    // memory, it is also async-signal-safe.
418
0
    if unsafe { libc::unshare(flags) } != 0 {
419
0
        return Err(Error::last_os_error());
420
0
    }
421
422
0
    if let Err(e) = write_signal_safe(c"/proc/self/setgroups", b"deny") {
423
        // If we fail to write this it will just make gid_map fail later,
424
        // but we may be able to continue anyway.
425
0
        if e != libc::EPERM && e != libc::EACCES && e != libc::ENOENT {
426
0
            return Err(Error::from_raw_os_error(e));
427
0
        }
428
0
    }
429
430
0
    let mut buffer = [0u8; 32];
431
0
    write_signal_safe(c"/proc/self/uid_map", create_map_line(uid, &mut buffer))
432
0
        .map_err(Error::from_raw_os_error)?;
433
434
    // If we can't write to gid_map, we just ignore it. This usually happens if
435
    // setgroups was not written to (because of permissions) or if we are in a
436
    // restricted environment.
437
0
    if let Err(e) = write_signal_safe(c"/proc/self/gid_map", create_map_line(gid, &mut buffer)) {
438
        // If this fails then we can probably continue just fine, it's just
439
        // the uid that's important.
440
0
        if e != libc::EPERM && e != libc::EACCES {
441
0
            return Err(Error::from_raw_os_error(e));
442
0
        }
443
0
    }
444
445
    // Configure the mount namespace if enabled.
446
0
    if mount {
447
0
        perform_remount(root_action_directory, action_directory).unwrap();
448
0
    }
449
450
    // Set hostname to "nativelink" to ensure reproducibility.
451
0
    let hostname = b"nativelink";
452
    // SAFETY: We reference the static memory above only and this is
453
    // async-signal-safe.
454
0
    if unsafe { libc::sethostname(hostname.as_ptr().cast(), hostname.len()) } != 0 {
455
        // SAFETY: We just called a libc function that failed.
456
0
        let err = unsafe { *libc::__errno_location() };
457
0
        if err != libc::EPERM && err != libc::EACCES {
458
0
            return Err(Error::from_raw_os_error(err));
459
0
        }
460
0
    }
461
462
    // Fork to enter the PID namespace.
463
    // SAFETY: We are already in a required async-signal-safe environment, we
464
    // will continue to ensure that ongoing.
465
0
    match unsafe { libc::fork() } {
466
        0 => {
467
            // SAFETY: This function is async-signal-safe and references no memory or resources.
468
0
            if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) } != 0 {
469
0
                exit(1);
470
0
            }
471
0
            Ok(())
472
        }
473
0
        pid if pid > 0 => {
474
            // Ensure that any children spawned by the action are re-parented to
475
            // this process if their parent dies. This is effectively a sub-reaper.
476
            // SAFETY: prctl is async-signal-safe.
477
0
            unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) };
478
479
            // SAFETY: All operations below simply _exit and therefore there
480
            // are no issues with dangling file descriptor handles.
481
0
            unsafe { close_all_fds() };
482
483
0
            let mut sigset = core::mem::MaybeUninit::<libc::sigset_t>::uninit();
484
            // SAFETY: sigset is on the stack and we are initializing it.
485
0
            unsafe {
486
0
                libc::sigemptyset(sigset.as_mut_ptr());
487
0
                libc::sigaddset(sigset.as_mut_ptr(), libc::SIGTERM);
488
0
                libc::sigaddset(sigset.as_mut_ptr(), libc::SIGCHLD);
489
0
                libc::sigprocmask(libc::SIG_BLOCK, sigset.as_ptr(), core::ptr::null_mut());
490
0
            }
491
492
            loop {
493
                // Reap all exited children.
494
                loop {
495
0
                    let mut status = 0;
496
                    // SAFETY: The status is on the stack and waitpid is otherwise
497
                    // safe to call.
498
0
                    let res = unsafe { libc::waitpid(-1, &raw mut status, libc::WNOHANG) };
499
0
                    if res == pid {
500
0
                        if libc::WIFEXITED(status) {
501
0
                            exit(libc::WEXITSTATUS(status));
502
0
                        } else if libc::WIFSIGNALED(status) {
503
                            // Try to exit with the same signal as the child.
504
                            // SAFETY: The sigset was previously allocated and used on the stack.
505
                            unsafe {
506
0
                                libc::sigprocmask(
507
                                    libc::SIG_UNBLOCK,
508
0
                                    sigset.as_ptr(),
509
0
                                    core::ptr::null_mut(),
510
                                )
511
                            };
512
                            // SAFETY: It's always safe to raise and as a fallback we _exit below.
513
0
                            unsafe { libc::raise(libc::WTERMSIG(status)) };
514
                            // We shouldn't get here, but it's a fallback in case.
515
0
                            exit(libc::WTERMSIG(status));
516
0
                        }
517
0
                    } else if res <= 0 {
518
                        // SAFETY: We just called a libc function that failed.
519
0
                        if res == -1 && unsafe { *libc::__errno_location() } != libc::EINTR {
520
0
                            exit(255);
521
0
                        }
522
                        // Break the reaping loop to wait for signals.
523
0
                        break;
524
0
                    }
525
                }
526
527
0
                let mut siginfo = core::mem::MaybeUninit::<libc::siginfo_t>::uninit();
528
                // SAFETY: sigset is initialized and siginfo is on the stack.
529
0
                let sig = unsafe { libc::sigwaitinfo(sigset.as_ptr(), siginfo.as_mut_ptr()) };
530
531
0
                if sig == libc::SIGTERM {
532
0
                    // SAFETY: pid is valid and we are sending a signal.
533
0
                    unsafe { libc::kill(pid, libc::SIGKILL) };
534
0
                }
535
            }
536
        }
537
0
        _ => Err(Error::last_os_error()),
538
    }
539
0
}