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