Skip to main content

hermit/
proc_mount.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//! Report the proc mount's capabilities after container setup.
10
11use nix::sys::statvfs::FsFlags;
12use nix::sys::statvfs::statvfs;
13use reverie::Errno;
14use reverie::Pid;
15
16/// One diagnostic per container launch, outside the captured guest output.
17pub const READONLY_WARNING: &str =
18    "hermit: warning: /proc is mounted read-only; guest writes to procfs may fail with EROFS.\n";
19
20fn write_warning(warning: &str) {
21    // While --max-log-bytes is in force, termination must not depend on this
22    // write: the loop below waits as long as a full stderr pipe stays unread.
23    // write_without_waiting makes one attempt that cannot block and drops the
24    // warning when stderr cannot take it at once. Like the loop, it makes only
25    // syscalls and signal-set operations, so it is safe in the pre_exec
26    // callback too; the flag it is chosen by was set before the fork.
27    if crate::nonwaiting_write::diagnostics_must_not_wait() {
28        crate::nonwaiting_write::write_without_waiting(libc::STDERR_FILENO, warning.as_bytes());
29        return;
30    }
31    // This also runs in the namespace-only pre_exec callback. Avoid allocation,
32    // stdio locks, and formatting after fork. Diagnostics are best effort if
33    // stderr is closed; a warning must not turn a successful setup into an error.
34    // Reverie restores SIGPIPE's default disposition before invoking us. Block
35    // it for this write and consume only a signal newly generated by our EPIPE,
36    // preserving both the caller's mask and any previously pending signal.
37    // SAFETY: the signal sets are initialized before use. These Linux signal
38    // operations use no allocation or userspace locks.
39    let (blocked, original, pending) = unsafe {
40        let mut blocked = std::mem::zeroed::<libc::sigset_t>();
41        let mut original = std::mem::zeroed::<libc::sigset_t>();
42        let mut pending = std::mem::zeroed::<libc::sigset_t>();
43        libc::sigemptyset(&mut blocked);
44        libc::sigaddset(&mut blocked, libc::SIGPIPE);
45        if libc::sigprocmask(libc::SIG_BLOCK, &blocked, &mut original) == -1 {
46            return;
47        }
48        if libc::sigpending(&mut pending) == -1 {
49            libc::sigprocmask(libc::SIG_SETMASK, &original, std::ptr::null_mut());
50            return;
51        }
52        (blocked, original, pending)
53    };
54    let mut broken_pipe = false;
55    let mut remaining = warning.as_bytes();
56    while !remaining.is_empty() {
57        // SAFETY: the message bytes remain valid for the complete write.
58        let written = unsafe {
59            libc::write(
60                libc::STDERR_FILENO,
61                remaining.as_ptr().cast(),
62                remaining.len(),
63            )
64        };
65        if written > 0 {
66            remaining = &remaining[written as usize..];
67        } else if written == -1 && Errno::last() == Errno::EINTR {
68            continue;
69        } else {
70            broken_pipe = written == -1 && Errno::last() == Errno::EPIPE;
71            break;
72        }
73    }
74    // SAFETY: consume only our new SIGPIPE without waiting, then restore the
75    // exact mask inherited by this thread before continuing guest startup.
76    unsafe {
77        if broken_pipe && libc::sigismember(&pending, libc::SIGPIPE) == 0 {
78            let timeout = libc::timespec {
79                tv_sec: 0,
80                tv_nsec: 0,
81            };
82            while libc::sigtimedwait(&blocked, std::ptr::null_mut(), &timeout) == -1
83                && Errno::last() == Errno::EINTR
84            {}
85        }
86        libc::sigprocmask(libc::SIG_SETMASK, &original, std::ptr::null_mut());
87    }
88}
89
90/// Inspect the completed container; safe to call in a pre_exec callback.
91pub fn warn_if_readonly_proc() {
92    let mut stats = std::mem::MaybeUninit::<libc::statfs64>::uninit();
93    // SAFETY: the pathname is NUL-terminated and statfs64 initializes the buffer
94    // on success. Linux statfs64 is a syscall wrapper, unlike libc statvfs
95    // compatibility paths that may allocate or lock between fork and exec.
96    if unsafe { libc::statfs64(c"/proc".as_ptr(), stats.as_mut_ptr()) } == -1 {
97        write_warning("hermit: warning: could not determine the /proc mount mode.\n");
98    } else if unsafe { stats.assume_init().f_flags } & libc::ST_RDONLY as libc::c_long != 0 {
99        write_warning(READONLY_WARNING);
100    }
101}
102
103/// The tracer is stopped after setup, so inspect its actual root rather than
104/// the controller's proc mount, which can have different capabilities.
105pub(crate) fn guest_proc_readonly(pid: Pid) -> Option<bool> {
106    match statvfs(format!("/proc/{pid}/root/proc").as_str()) {
107        Ok(stats) => Some(stats.flags().contains(FsFlags::ST_RDONLY)),
108        Err(error) => {
109            write_warning(&format!(
110                "hermit: warning: could not determine the guest /proc mount mode: {error}\n"
111            ));
112            None
113        }
114    }
115}
116
117pub(crate) fn report_proc_mode(recorded: Option<bool>, actual: Option<bool>) {
118    if let (Some(recorded), Some(actual)) = (recorded, actual)
119        && recorded != actual
120    {
121        let mode = |readonly| if readonly { "read-only" } else { "read-write" };
122        write_warning(&format!(
123            "hermit: warning: /proc mount mode differs: recording was {}, replay is {}; \
124             guest procfs writes may behave differently.\n",
125            mode(recorded),
126            mode(actual)
127        ));
128    } else if actual == Some(true) {
129        write_warning(READONLY_WARNING);
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use std::os::fd::AsRawFd;
136    use std::os::fd::FromRawFd;
137    use std::os::fd::OwnedFd;
138
139    use reverie::process::Container;
140
141    #[test]
142    fn readonly_proc_warning_survives_closed_stderr() {
143        if crate::run_in_fresh_process(
144            "proc_mount::tests::readonly_proc_warning_survives_closed_stderr",
145        ) {
146            return;
147        }
148        let mut descriptors = [-1; 2];
149        // SAFETY: pipe2 initializes two distinct owned descriptors on success.
150        assert_eq!(
151            unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) },
152            0,
153            "create stderr pipe: {}",
154            std::io::Error::last_os_error()
155        );
156        // SAFETY: ownership of each fresh descriptor is transferred once.
157        let (reader, writer) = unsafe {
158            (
159                OwnedFd::from_raw_fd(descriptors[0]),
160                OwnedFd::from_raw_fd(descriptors[1]),
161            )
162        };
163        drop(reader);
164
165        // Do not create a PID namespace: its init process is protected from an
166        // unhandled SIGPIPE and would let an unsafe warning writer pass.
167        let observation = Container::new()
168            .run(|| {
169                // SAFETY: all descriptor and signal changes affect only this
170                // child. The signal sets are initialized before use.
171                unsafe {
172                    let mut action = std::mem::zeroed::<libc::sigaction>();
173                    action.sa_sigaction = libc::SIG_DFL;
174                    libc::sigemptyset(&mut action.sa_mask);
175                    let mut signals = std::mem::zeroed::<libc::sigset_t>();
176                    libc::sigemptyset(&mut signals);
177                    libc::sigaddset(&mut signals, libc::SIGPIPE);
178                    if libc::dup2(writer.as_raw_fd(), libc::STDERR_FILENO) == -1
179                        || libc::sigaction(libc::SIGPIPE, &action, std::ptr::null_mut()) == -1
180                        || libc::sigprocmask(libc::SIG_UNBLOCK, &signals, std::ptr::null_mut())
181                            == -1
182                    {
183                        return Err(*libc::__errno_location());
184                    }
185
186                    super::report_proc_mode(None, Some(true));
187
188                    let mut mask = std::mem::zeroed::<libc::sigset_t>();
189                    let mut pending = std::mem::zeroed::<libc::sigset_t>();
190                    if libc::sigprocmask(libc::SIG_BLOCK, std::ptr::null(), &mut mask) == -1
191                        || libc::sigpending(&mut pending) == -1
192                        || libc::sigaction(libc::SIGPIPE, std::ptr::null(), &mut action) == -1
193                    {
194                        return Err(*libc::__errno_location());
195                    }
196                    Ok((
197                        libc::getpid(),
198                        action.sa_sigaction == libc::SIG_DFL,
199                        libc::sigismember(&mask, libc::SIGPIPE) == 0,
200                        libc::sigismember(&pending, libc::SIGPIPE) == 0,
201                    ))
202                }
203            })
204            .expect("the warning must not terminate the child with SIGPIPE")
205            .expect("configure and inspect child stderr and signal state");
206        assert!(
207            observation.0 > 1,
208            "the child must not be PID namespace init"
209        );
210        assert_eq!(
211            (observation.1, observation.2, observation.3),
212            (true, true, true),
213            "the warning must preserve default/unblocked SIGPIPE and consume its own pending signal"
214        );
215    }
216}