Skip to main content

detcore/
nonwaiting_write.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Diagnostic writes that never wait for the reader of a descriptor.
10//!
11//! While `--max-log-bytes` is in force, termination of a run must not depend
12//! on any diagnostic write (review of
13//! <https://github.com/rrnewton/hermit/pull/3686>). A plain `write(2)` to a
14//! blocking stderr whose reader stopped reading waits forever, so every
15//! hermit diagnostic that can run in a capped run before the cap or the 123
16//! classification goes through [`write_without_waiting`] once
17//! [`forbid_waiting_diagnostics`] has been called.
18//!
19//! [`crate::util::RetryingStderr`] reads the same flag: while it is set, its
20//! writes go through [`write_without_waiting_for_a_reader`] and wait only
21//! within its bounded deadline, never inside `write(2)`.
22//!
23//! This lives in detcore, below the `hermit` library and binary, because
24//! detcore's own stderr writer reads the flag, and because `hermit`'s
25//! `proc_mount` writes its warning from the namespace-only `pre_exec` callback,
26//! after `fork`, and from the container init: every function here except
27//! [`write_without_waiting_for_a_reader`] makes only syscalls and signal-set
28//! operations, without allocation, stdio locks or formatting.
29
30use std::io;
31use std::mem;
32use std::os::fd::RawFd;
33use std::sync::atomic::AtomicBool;
34use std::sync::atomic::Ordering;
35
36/// Set once, before any fork, by the `hermit` binary when `--max-log-bytes`
37/// is in force. A forked child (the container init, a `pre_exec` callback)
38/// inherits the value with the rest of its memory.
39static DIAGNOSTICS_MUST_NOT_WAIT: AtomicBool = AtomicBool::new(false);
40
41/// From now on, in this process and every process it forks, a diagnostic that
42/// asks [`diagnostics_must_not_wait`] is written once with
43/// [`write_without_waiting`] and omitted when stderr cannot take it at once,
44/// and [`crate::util::RetryingStderr`] never waits inside `write(2)`.
45/// Only `hermit`'s `main` calls this, when `--max-log-bytes` is given.
46pub fn forbid_waiting_diagnostics() {
47    DIAGNOSTICS_MUST_NOT_WAIT.store(true, Ordering::Relaxed);
48}
49
50/// Whether [`forbid_waiting_diagnostics`] was called. A single atomic load, so
51/// it is safe after `fork` and in a signal handler.
52pub fn diagnostics_must_not_wait() -> bool {
53    DIAGNOSTICS_MUST_NOT_WAIT.load(Ordering::Relaxed)
54}
55
56/// Put `bytes` on `fd` once, without waiting for the other end and without
57/// letting the attempt raise a signal. A diagnostic that cannot be delivered
58/// that way is omitted: the exit status still says why the run ended.
59///
60/// THE DESCRIPTOR ITSELF IS NEVER MADE NON-BLOCKING. An inherited descriptor's
61/// open file description is shared with the parent, the guest or a terminal,
62/// and `fcntl(O_NONBLOCK)` on it would leak into all of them. Each kind of file
63/// instead gets a primitive that is non-blocking on its own:
64///
65/// - a regular file: `pwritev2(RWF_NOWAIT)` at offset -1, so the file position
66///   and `O_APPEND` are honoured. `EAGAIN` or `EOPNOTSUPP` omit the line.
67///   Buffered `RWF_NOWAIT` writes fail with `EAGAIN` on btrfs and with
68///   `EOPNOTSUPP` on tmpfs, so a log file there does not get the line.
69/// - a socket: `send(MSG_DONTWAIT | MSG_NOSIGNAL)`.
70/// - a pipe, FIFO or terminal: a NEW open file description for the same
71///   object, opened through `/proc/self/fd/<fd>` with `O_NONBLOCK`, and one
72///   write of at most `PIPE_BUF` bytes, which a pipe either takes whole or
73///   refuses with `EAGAIN`. Opening a pipe that has no reader fails with
74///   `ENXIO` rather than raising `SIGPIPE`.
75/// - anything else, including a descriptor whose type cannot be learned:
76///   omitted.
77///
78/// The file type comes from `file_type` in this module.
79///
80/// No signal escapes; see [`suppressing_diagnostic_signals`].
81///
82/// What this cannot avoid are short kernel locks that sleep uninterruptibly,
83/// which no handled signal or timer could break either: the file-position lock
84/// of an open file description another process is writing through at that
85/// moment, a terminal's termios and output locks, a socket's lock. None of
86/// them waits for a reader to drain anything.
87pub fn write_without_waiting(fd: RawFd, bytes: &[u8]) {
88    let bytes = &bytes[..bytes.len().min(libc::PIPE_BUF)];
89    suppressing_diagnostic_signals(|| {
90        let Ok(file_type) = file_type(fd) else {
91            return 0;
92        };
93        let written = match file_type {
94            libc::S_IFREG => {
95                let iov = libc::iovec {
96                    iov_base: bytes.as_ptr() as *mut libc::c_void,
97                    iov_len: bytes.len(),
98                };
99                // SAFETY: one valid iovec over `bytes`; offset -1 is the
100                // current position.
101                unsafe { libc::pwritev2(fd, &iov, 1, -1, libc::RWF_NOWAIT) }
102            }
103            // SAFETY: `bytes` is valid for its length.
104            libc::S_IFSOCK => unsafe {
105                libc::send(
106                    fd,
107                    bytes.as_ptr().cast(),
108                    bytes.len(),
109                    libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL,
110                )
111            },
112            libc::S_IFIFO => return write_through_new_description(fd, bytes),
113            // SAFETY: isatty only inspects `fd`.
114            libc::S_IFCHR if unsafe { libc::isatty(fd) } == 1 => {
115                return write_through_new_description(fd, bytes);
116            }
117            _ => return 0,
118        };
119        if written < 0 { last_errno() } else { 0 }
120    });
121}
122
123/// One write of `bytes` to `fd` that cannot wait for a reader to drain the
124/// file, for a writer that then waits itself, within its own bound, when it
125/// is told the file is full ([`crate::util::RetryingStderr`] while
126/// [`diagnostics_must_not_wait`]). Returns the bytes written, which may be
127/// fewer than `bytes.len()`, or the error; `WouldBlock` means full.
128///
129/// THE DESCRIPTOR ITSELF IS NEVER MADE NON-BLOCKING, as in
130/// [`write_without_waiting`]:
131///
132/// - a pipe, a FIFO or any character device, a terminal or `/dev/null`: one
133///   write through a new `O_NONBLOCK` open file description for the same
134///   object (see [`write_without_waiting`]). Opening a pipe that has no reader
135///   fails with `ENXIO`, reported here as `EPIPE`, what a write to it would
136///   have returned. If the new description cannot be opened, nothing is
137///   written and the error is returned.
138///
139///   ⚠️ NO CHARACTER DEVICE GETS A PLAIN WRITE, not even one the terminal
140///   query says is not a terminal. Nothing the process asks can tell a
141///   terminal from `/dev/null` in a way a seccomp policy cannot forge: a
142///   policy that answers the query with `ENOTTY` makes a terminal look like
143///   `/dev/null`, and a plain write to a full terminal waits for a reader
144///   forever.
145/// - a socket: `send(MSG_DONTWAIT | MSG_NOSIGNAL)`.
146/// - anything else, a regular file included: a plain `write(2)`. Nothing
147///   reads a regular file, so the write cannot wait for a reader to drain it;
148///   and `RWF_NOWAIT` would refuse buffered writes on btrfs and tmpfs, which
149///   would lose every line of a log written there. A regular file on a
150///   network or FUSE file system whose server has stopped answering can
151///   still hold this write.
152/// - a descriptor whose type cannot be learned (see `file_type` in this
153///   module): nothing is written, and the error is returned, so the caller
154///   gives up on the bytes.
155///
156/// No `SIGPIPE`, `SIGXFSZ` or `SIGTTOU` escapes; see
157/// [`suppressing_diagnostic_signals`].
158pub fn write_without_waiting_for_a_reader(fd: RawFd, bytes: &[u8]) -> io::Result<usize> {
159    // Replaced by the closure; left only if the signal mask could not be set,
160    // in which case nothing was written.
161    let mut result = Err(io::Error::other(
162        "the signal mask for a diagnostic write could not be set",
163    ));
164    suppressing_diagnostic_signals(|| {
165        let file_type = match file_type(fd) {
166            Ok(file_type) => file_type,
167            Err(errno) => {
168                // ⚠️ FAIL CLOSED. Without the file type there is no way to
169                // tell a pipe from a regular file, and a plain write to a full
170                // pipe waits forever. The line is not written; the caller
171                // gives up on it.
172                result = Err(io::Error::from_raw_os_error(errno));
173                return 0;
174            }
175        };
176        let written = match file_type {
177            // SAFETY: `bytes` is valid for its length.
178            libc::S_IFSOCK => unsafe {
179                libc::send(
180                    fd,
181                    bytes.as_ptr().cast(),
182                    bytes.len(),
183                    libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL,
184                )
185            },
186            libc::S_IFIFO | libc::S_IFCHR => {
187                let path = proc_self_fd_path(fd);
188                // SAFETY: `path` is NUL-terminated.
189                let reopened = unsafe {
190                    libc::open(
191                        path.as_ptr().cast(),
192                        libc::O_WRONLY | libc::O_NONBLOCK | libc::O_CLOEXEC | libc::O_NOCTTY,
193                    )
194                };
195                if reopened < 0 {
196                    let errno = match last_errno() {
197                        libc::ENXIO => libc::EPIPE,
198                        errno => errno,
199                    };
200                    result = Err(io::Error::from_raw_os_error(errno));
201                    // Nothing was written, so no signal was generated.
202                    return 0;
203                }
204                // SAFETY: `bytes` is valid for its length; `reopened` is ours.
205                let written = unsafe { libc::write(reopened, bytes.as_ptr().cast(), bytes.len()) };
206                let errno = last_errno();
207                // SAFETY: closes only the descriptor opened above.
208                unsafe { libc::close(reopened) };
209                if written < 0 {
210                    result = Err(io::Error::from_raw_os_error(errno));
211                    return errno;
212                }
213                written
214            }
215            // SAFETY: `bytes` is valid for its length.
216            _ => unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) },
217        };
218        if written < 0 {
219            let error = io::Error::last_os_error();
220            let errno = error.raw_os_error().unwrap_or(0);
221            result = Err(error);
222            errno
223        } else {
224            result = Ok(written as usize);
225            0
226        }
227    });
228    result
229}
230
231/// The type of the file behind `fd`, its nonzero `S_IFMT` bits, or the errno
232/// that kept it from being learned.
233///
234/// `statx(AT_STATX_DONT_SYNC)` first: it answers from cached attributes
235/// instead of asking a network or FUSE file system. When `statx` fails (a
236/// seccomp policy that denies it, as the Rust standard library also allows
237/// for) or names no type, two questions that the open file answers from
238/// memory: `fcntl(F_GETPIPE_SZ)`, which only a pipe or a FIFO answers, and
239/// `getsockopt(SO_TYPE)`, which only a socket answers. Any other file then
240/// has no type here, and the writers fail closed. The error is `statx`'s, or
241/// `EIO` when `statx` returned 0 without a type.
242///
243/// ⚠️ NO `fstat`. It asks a network or FUSE file system for attributes once
244/// its cached ones have expired, and waits for the server's answer; a server
245/// that has stopped answering, behind a FIFO or a regular file on stderr,
246/// would then hold the crossing writer before `_exit(123)`, and the stderr
247/// log sink before the cap is crossed.
248///
249/// ⚠️ A CALL THAT RETURNS 0 BUT NAMES NO TYPE HAS FAILED. A seccomp policy
250/// that answers with errno 0 makes the call return 0 and fill in nothing, and
251/// a zeroed type would read as "anything else", which
252/// [`write_without_waiting_for_a_reader`] writes to plainly. For the same
253/// reason a pipe size of 0 and a socket type of 0 are not answers: no pipe
254/// holds 0 bytes, and no socket type is 0.
255fn file_type(fd: RawFd) -> Result<libc::mode_t, i32> {
256    // SAFETY: statx writes only into the zeroed struct it is given; an empty
257    // path with AT_EMPTY_PATH names `fd` itself.
258    let mut stat: reverie::syscalls::linux_abi::Statx = unsafe { mem::zeroed() };
259    let inspected = unsafe {
260        reverie::syscalls::linux_abi::statx(
261            fd,
262            c"".as_ptr(),
263            libc::AT_EMPTY_PATH | reverie::syscalls::linux_abi::AT_STATX_DONT_SYNC,
264            reverie::syscalls::linux_abi::STATX_TYPE,
265            &mut stat,
266        )
267    } == 0;
268    let statx_errno = if inspected { libc::EIO } else { last_errno() };
269    let file_type = libc::mode_t::from(stat.stx_mode) & libc::S_IFMT;
270    if inspected && stat.stx_mask & reverie::syscalls::linux_abi::STATX_TYPE != 0 && file_type != 0
271    {
272        return Ok(file_type);
273    }
274    // SAFETY: F_GETPIPE_SZ only reads the pipe behind `fd`.
275    if unsafe { libc::fcntl(fd, libc::F_GETPIPE_SZ) } > 0 {
276        return Ok(libc::S_IFIFO);
277    }
278    let mut socket_type: libc::c_int = 0;
279    let mut length = mem::size_of::<libc::c_int>() as libc::socklen_t;
280    // SAFETY: getsockopt writes at most `length` bytes into `socket_type`.
281    let answered = unsafe {
282        libc::getsockopt(
283            fd,
284            libc::SOL_SOCKET,
285            libc::SO_TYPE,
286            (&raw mut socket_type).cast(),
287            &mut length,
288        )
289    } == 0;
290    if answered && socket_type != 0 {
291        return Ok(libc::S_IFSOCK);
292    }
293    Err(statx_errno)
294}
295
296/// The calling thread's `errno`, 0 when there is none.
297pub fn last_errno() -> i32 {
298    io::Error::last_os_error().raw_os_error().unwrap_or(0)
299}
300
301/// Write `bytes` through a new, non-blocking open file description for the
302/// pipe, FIFO or terminal behind `fd`. Returns the write's errno, 0 for none.
303/// `O_NOCTTY`: a session leader (the container init) must not acquire the
304/// terminal as its controlling terminal by writing a diagnostic to it.
305fn write_through_new_description(fd: RawFd, bytes: &[u8]) -> i32 {
306    let path = proc_self_fd_path(fd);
307    // SAFETY: `path` is NUL-terminated.
308    let reopened = unsafe {
309        libc::open(
310            path.as_ptr().cast(),
311            libc::O_WRONLY | libc::O_NONBLOCK | libc::O_CLOEXEC | libc::O_NOCTTY,
312        )
313    };
314    if reopened < 0 {
315        // ENXIO is a pipe with no reader: nothing written, nothing raised.
316        return 0;
317    }
318    // SAFETY: `bytes` is valid for its length; `reopened` is ours.
319    let written = unsafe { libc::write(reopened, bytes.as_ptr().cast(), bytes.len()) };
320    let errno = if written < 0 { last_errno() } else { 0 };
321    // SAFETY: closes only the descriptor opened above.
322    unsafe { libc::close(reopened) };
323    errno
324}
325
326/// `/proc/self/fd/<fd>` as a NUL-terminated path, built without allocating:
327/// the crossing writer may run with arbitrary locks held.
328pub fn proc_self_fd_path(fd: RawFd) -> [u8; 32] {
329    const PREFIX: &[u8] = b"/proc/self/fd/";
330    let mut path = [0u8; 32];
331    path[..PREFIX.len()].copy_from_slice(PREFIX);
332    let mut digits = [0u8; 10];
333    let mut count = 0;
334    let mut rest = fd.unsigned_abs();
335    loop {
336        digits[count] = b'0' + (rest % 10) as u8;
337        count += 1;
338        rest /= 10;
339        if rest == 0 {
340            break;
341        }
342    }
343    for (slot, digit) in path[PREFIX.len()..]
344        .iter_mut()
345        .zip(digits[..count].iter().rev())
346    {
347        *slot = *digit;
348    }
349    path
350}
351
352/// Run one diagnostic write (`write` returns its errno, 0 for none) so that no
353/// signal it generates escapes.
354///
355/// Reverie's container setup restores `SIGPIPE`'s default disposition and
356/// clears the signal mask in the process that runs the tracer. Under
357/// `--no-namespace` that was an ordinary process, and round 2 of the review of
358/// <https://github.com/rrnewton/hermit/pull/3686> found a `SIGPIPE` from its
359/// crossing line killing it before `_exit(123)`, reported as an internal
360/// failure (125); the cap is refused there now. In hermit's PID namespace the
361/// tracer runs in the container init, PID 1 of the namespace, and the kernel
362/// discards a signal at its default disposition that the init raises in
363/// itself, so there the guard is a defence rather than the fix: it keeps any
364/// process in which this writer runs with `SIGPIPE` at its default from dying
365/// before the exit.
366/// As in `proc_mount`'s warning writer, `SIGPIPE` and `SIGXFSZ` (a file-size
367/// limit) are blocked for the attempt, and a signal is consumed only when this
368/// write failed with the matching error and the signal was not already pending
369/// before it. `SIGTTOU` is blocked too: a terminal with `TOSTOP` treats a
370/// blocked `SIGTTOU` as ignored and accepts the write, instead of stopping a
371/// background process. The original mask is restored afterwards.
372pub fn suppressing_diagnostic_signals(write: impl FnOnce() -> i32) {
373    // SAFETY: sigset operations on local, initialized sets; pthread_sigmask and
374    // sigtimedwait affect only the calling thread.
375    unsafe {
376        let mut blocked: libc::sigset_t = mem::zeroed();
377        libc::sigemptyset(&mut blocked);
378        for signal in [libc::SIGPIPE, libc::SIGXFSZ, libc::SIGTTOU] {
379            libc::sigaddset(&mut blocked, signal);
380        }
381        let mut original: libc::sigset_t = mem::zeroed();
382        if libc::pthread_sigmask(libc::SIG_BLOCK, &blocked, &mut original) != 0 {
383            return;
384        }
385        let mut pending_before: libc::sigset_t = mem::zeroed();
386        libc::sigemptyset(&mut pending_before);
387        libc::sigpending(&mut pending_before);
388        let errno = write();
389        let generated = match errno {
390            libc::EPIPE => Some(libc::SIGPIPE),
391            libc::EFBIG => Some(libc::SIGXFSZ),
392            _ => None,
393        };
394        if let Some(signal) = generated
395            && libc::sigismember(&pending_before, signal) == 0
396        {
397            let mut consume: libc::sigset_t = mem::zeroed();
398            libc::sigemptyset(&mut consume);
399            libc::sigaddset(&mut consume, signal);
400            let now = libc::timespec {
401                tv_sec: 0,
402                tv_nsec: 0,
403            };
404            while libc::sigtimedwait(&consume, std::ptr::null_mut(), &now) < 0
405                && last_errno() == libc::EINTR
406            {}
407        }
408        libc::pthread_sigmask(libc::SIG_SETMASK, &original, std::ptr::null_mut());
409    }
410}