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}