Skip to main content

miri/shims/native_lib/trace/
child.rs

1use std::cell::RefCell;
2use std::panic::abort_on_unwind;
3use std::ptr::NonNull;
4use std::rc::Rc;
5
6use ipc_channel::{TryRecvError, ipc};
7use nix::sys::{mman, ptrace, signal};
8use nix::unistd;
9use rustc_const_eval::interpret::{InterpResult, interp_ok};
10
11use super::CALLBACK_STACK_SIZE;
12use super::messages::{Confirmation, StartFfiInfo, TraceRequest};
13use super::parent::{ChildListener, sv_loop};
14use crate::alloc::isolated_alloc::IsolatedAlloc;
15use crate::shims::native_lib::MemEvents;
16
17/// A handle to the single, shared supervisor process across all `MiriMachine`s.
18/// Since it would be very difficult to trace multiple FFI calls in parallel, we
19/// need to ensure that either (a) only one `MiriMachine` is performing an FFI call
20/// at any given time, or (b) there are distinct supervisor and child processes for
21/// each machine. The former was chosen here.
22///
23/// This should only contain a `None` if the supervisor has not (yet) been initialised;
24/// otherwise, if `init_sv` was called and did not error, this will always be nonempty.
25static SUPERVISOR: std::sync::Mutex<Option<Supervisor>> = std::sync::Mutex::new(None);
26
27/// The main means of communication between the child and parent process,
28/// allowing the former to send requests and get info from the latter.
29pub struct Supervisor {
30    /// Sender for FFI-mode-related requests.
31    message_tx: ipc::IpcSender<TraceRequest>,
32    /// Used for synchronisation, allowing us to receive confirmation that the
33    /// parent process has handled the request from `message_tx`.
34    confirm_rx: ipc::IpcReceiver<Confirmation>,
35    /// Receiver for memory accesses that occurred during the FFI call.
36    event_rx: ipc::IpcReceiver<MemEvents>,
37}
38
39/// Marker representing that an error occurred during creation of the supervisor.
40#[derive(Debug)]
41pub struct SvInitError;
42
43impl Supervisor {
44    /// Returns `true` if the supervisor process exists, and `false` otherwise.
45    pub fn is_enabled() -> bool {
46        SUPERVISOR.lock().unwrap().is_some()
47    }
48
49    unsafe fn protect_pages(
50        pages: impl Iterator<Item = (NonNull<u8>, usize)>,
51        prot: mman::ProtFlags,
52    ) -> Result<(), nix::errno::Errno> {
53        for (pg, sz) in pages {
54            unsafe { mman::mprotect(pg.cast(), sz, prot)? };
55        }
56        Ok(())
57    }
58
59    /// Performs an arbitrary FFI call, enabling tracing from the supervisor.
60    /// As this locks the supervisor via a mutex, no other threads may enter FFI
61    /// until this function returns.
62    pub fn do_ffi<'tcx, T>(
63        alloc: &Rc<RefCell<IsolatedAlloc>>,
64        f: impl FnOnce() -> T,
65    ) -> InterpResult<'tcx, (T, Option<MemEvents>)> {
66        let mut sv_guard = SUPERVISOR.lock().unwrap();
67        // If the supervisor is not initialised for whatever reason, fast-return.
68        // As a side-effect, even on platforms where ptracing
69        // is not implemented, we enforce that only one FFI call
70        // happens at a time.
71        let Some(sv) = sv_guard.as_mut() else { return interp_ok((f(), None)) };
72
73        // Get pointers to all the pages the supervisor must allow accesses in
74        // and prepare the callback stack.
75        let alloc = alloc.borrow();
76        let page_size = alloc.page_size();
77        let page_ptrs = alloc
78            .pages()
79            .flat_map(|(pg, sz)| {
80                // Convert (page, size) pair into list of pages.
81                let start = pg.expose_provenance().get();
82                (0..sz.strict_div(alloc.page_size()))
83                    .map(move |i| start.strict_add(i.strict_mul(page_size)))
84            })
85            .collect();
86        let raw_stack_ptr: *mut [u8; CALLBACK_STACK_SIZE] =
87            Box::leak(Box::new([0u8; CALLBACK_STACK_SIZE])).as_mut_ptr().cast();
88        let stack_ptr = raw_stack_ptr.expose_provenance();
89        let start_info = StartFfiInfo { page_ptrs, stack_ptr };
90
91        // Unwinding might be messed up due to partly protected memory, so let's abort if something
92        // breaks inside here.
93        let res = abort_on_unwind(|| {
94            // Send over the info.
95            // NB: if we do not wait to receive a blank confirmation response, it is
96            // possible that the supervisor is alerted of the SIGSTOP *before* it has
97            // actually received the start_info, thus deadlocking! This way, we can
98            // enforce an ordering for these events.
99            sv.message_tx.send(TraceRequest::StartFfi(start_info)).unwrap();
100            sv.confirm_rx.recv().unwrap();
101            // We need to be stopped for the supervisor to be able to make certain
102            // modifications to our memory - simply waiting on the recv() doesn't
103            // count.
104            signal::raise(signal::SIGSTOP).unwrap();
105
106            // SAFETY: We have coordinated with the supervisor to ensure that this memory will keep
107            // working as normal, just with extra tracing. So even if the compiler moves memory
108            // accesses down to after the `mprotect`, they won't actually segfault.
109            unsafe {
110                Self::protect_pages(alloc.pages(), mman::ProtFlags::PROT_NONE).unwrap();
111            }
112
113            let res = f();
114
115            // SAFETY: We set memory back to normal, so this is safe.
116            unsafe {
117                Self::protect_pages(
118                    alloc.pages(),
119                    mman::ProtFlags::PROT_READ | mman::ProtFlags::PROT_WRITE,
120                )
121                .unwrap();
122            }
123
124            // Signal the supervisor that we are done. Will block until the supervisor continues us.
125            // This will also shut down the segfault handler, so it's important that all memory is
126            // reset back to normal above. There must not be a window in time where accessing the
127            // pages we protected above actually causes the program to abort.
128            signal::raise(signal::SIGUSR1).unwrap();
129
130            res
131        });
132
133        // SAFETY: Caller upholds that this pointer was allocated as a box with
134        // this type.
135        unsafe {
136            drop(Box::from_raw(raw_stack_ptr));
137        }
138        // On the off-chance something really weird happens, don't block forever.
139        let events = sv
140            .event_rx
141            .try_recv_timeout(std::time::Duration::from_secs(5))
142            .map_err(|e| {
143                match e {
144                    TryRecvError::IpcError(_) => (),
145                    TryRecvError::Empty =>
146                        panic!("Waiting for accesses from supervisor timed out!"),
147                }
148            })
149            .ok();
150
151        interp_ok((res, events))
152    }
153}
154
155/// Initialises the supervisor process. If this function errors, then the
156/// supervisor process could not be created successfully; else, the caller
157/// is now the child process and can communicate via `do_ffi`, receiving back
158/// events at the end.
159///
160/// # Safety
161/// The invariants for `fork()` must be upheld by the caller, namely either:
162/// - Other threads do not exist, or;
163/// - If they do exist, either those threads or the resulting child process
164///   only ever act in [async-signal-safe](https://www.man7.org/linux/man-pages/man7/signal-safety.7.html) ways.
165pub unsafe fn init_sv() -> Result<(), SvInitError> {
166    // FIXME: Much of this could be reimplemented via the mitosis crate if we upstream the
167    // relevant missing bits.
168
169    // On Linux, this will check whether ptrace is fully disabled by the Yama module.
170    // If Yama isn't running or we're not on Linux, we'll still error later, but
171    // this saves a very expensive fork call.
172    let ptrace_status = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope");
173    if let Ok(stat) = ptrace_status {
174        if let Some(stat) = stat.chars().next() {
175            // Fast-error if ptrace is fully disabled on the system.
176            if stat == '3' {
177                return Err(SvInitError);
178            }
179        }
180    }
181
182    // Initialise the supervisor if it isn't already, placing it into SUPERVISOR.
183    let mut lock = SUPERVISOR.lock().unwrap();
184    if lock.is_some() {
185        return Ok(());
186    }
187
188    // Prepare the IPC channels we need.
189    let (message_tx, message_rx) = ipc::channel().unwrap();
190    let (confirm_tx, confirm_rx) = ipc::channel().unwrap();
191    let (event_tx, event_rx) = ipc::channel().unwrap();
192    // SAFETY: Calling sysconf(_SC_PAGESIZE) is always safe and cannot error.
193    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.try_into().unwrap();
194    super::parent::PAGE_SIZE.store(page_size, std::sync::atomic::Ordering::Relaxed);
195
196    unsafe {
197        // TODO: Maybe use clone3() instead for better signalling of when the child exits?
198        // SAFETY: Caller upholds that only one thread exists.
199        match unistd::fork().unwrap() {
200            unistd::ForkResult::Parent { child } => {
201                // If somehow another thread does exist, prevent it from accessing the lock
202                // and thus breaking our safety invariants.
203                std::mem::forget(lock);
204                // The child process is free to unwind, so we won't to avoid doubly freeing
205                // system resources.
206                let init = std::panic::catch_unwind(|| {
207                    let listener = ChildListener::new(message_rx, confirm_tx.clone());
208                    // Trace as many things as possible, to be able to handle them as needed.
209                    let options = ptrace::Options::PTRACE_O_TRACESYSGOOD
210                        | ptrace::Options::PTRACE_O_TRACECLONE
211                        | ptrace::Options::PTRACE_O_TRACEFORK;
212                    // Attach to the child process without stopping it.
213                    match ptrace::seize(child, options) {
214                        // Ptrace works :D
215                        Ok(_) => {
216                            let code = sv_loop(listener, child, event_tx, confirm_tx).unwrap_err();
217                            // If a return code of 0 is not explicitly given, assume something went
218                            // wrong and return 1.
219                            std::process::exit(code.0.unwrap_or(1))
220                        }
221                        // Ptrace does not work and we failed to catch that.
222                        Err(_) => {
223                            // If we can't ptrace, Miri continues being the parent.
224                            signal::kill(child, signal::SIGKILL).unwrap();
225                            SvInitError
226                        }
227                    }
228                });
229                match init {
230                    // The "Ok" case means that we couldn't ptrace.
231                    Ok(e) => return Err(e),
232                    Err(_p) => {
233                        eprintln!(
234                            "Supervisor process panicked!\n\"
235                            Try running again without `-Zmiri-native-lib-enable-tracing`."
236                        );
237                        std::process::exit(1);
238                    }
239                }
240            }
241            unistd::ForkResult::Child => {
242                // Make sure we never get orphaned and stuck in SIGSTOP or similar
243                // SAFETY: prctl PR_SET_PDEATHSIG is always safe to call.
244                let ret = libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
245                assert_eq!(ret, 0);
246                // First make sure the parent succeeded with ptracing us!
247                signal::raise(signal::SIGSTOP).unwrap();
248                // If we're the child process, save the supervisor info.
249                *lock = Some(Supervisor { message_tx, confirm_rx, event_rx });
250            }
251        }
252    }
253    Ok(())
254}
255
256/// Instruct the supervisor process to return a particular code. Useful if for
257/// whatever reason this code fails to be intercepted normally.
258pub fn register_retcode_sv(code: i32) {
259    let mut sv_guard = SUPERVISOR.lock().unwrap();
260    if let Some(sv) = sv_guard.as_mut() {
261        sv.message_tx.send(TraceRequest::OverrideRetcode(code)).unwrap();
262        sv.confirm_rx.recv().unwrap();
263    }
264}