Coverage Report

Created: 2026-07-21 15:28

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