Skip to content

Commit b8d1e70

Browse files
committed
supervisor bits of ffi ptracing
1 parent 2f4f9ac commit b8d1e70

File tree

6 files changed

+606
-81
lines changed

6 files changed

+606
-81
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ libloading = "0.8"
4444
nix = { version = "0.30.1", features = ["mman", "ptrace", "signal"] }
4545
ipc-channel = "0.19.0"
4646
serde = { version = "1.0.219", features = ["derive"] }
47+
capstone = "0.13"
4748

4849
[dev-dependencies]
4950
ui_test = "0.29.1"

src/alloc/isolated_alloc.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::alloc::{self, Layout};
22

3+
use nix::sys::mman;
34
use rustc_index::bit_set::DenseBitSet;
45

56
/// How many bytes of memory each bit in the bitset represents.
@@ -271,13 +272,54 @@ impl IsolatedAlloc {
271272
pub fn pages(&self) -> Vec<usize> {
272273
let mut pages: Vec<_> =
273274
self.page_ptrs.clone().into_iter().map(|p| p.expose_provenance()).collect();
274-
for (ptr, size) in &self.huge_ptrs {
275+
self.huge_ptrs.iter().for_each(|(ptr, size)| {
275276
for i in 0..size / self.page_size {
276277
pages.push(ptr.expose_provenance().strict_add(i * self.page_size));
277278
}
278-
}
279+
});
279280
pages
280281
}
282+
283+
/// Protects all owned memory as `PROT_NONE`, preventing accesses.
284+
///
285+
/// SAFETY: Accessing memory after this point will result in a segfault
286+
/// unless it is first unprotected.
287+
pub unsafe fn prepare_ffi(&mut self) -> Result<(), nix::errno::Errno> {
288+
let prot = mman::ProtFlags::PROT_NONE;
289+
unsafe { self.mprotect(prot) }
290+
}
291+
292+
/// Deprotects all owned memory by setting it to RW. Erroring here is very
293+
/// likely unrecoverable, so it may panic if applying those permissions
294+
/// fails.
295+
pub fn unprep_ffi(&mut self) {
296+
let prot = mman::ProtFlags::PROT_READ | mman::ProtFlags::PROT_WRITE;
297+
unsafe {
298+
self.mprotect(prot).unwrap();
299+
}
300+
}
301+
302+
/// Applies `prot` to every page managed by the allocator.
303+
///
304+
/// SAFETY: Accessing memory in violation of the protection flags will
305+
/// trigger a segfault.
306+
unsafe fn mprotect(&mut self, prot: mman::ProtFlags) -> Result<(), nix::errno::Errno> {
307+
for &pg in &self.page_ptrs {
308+
unsafe {
309+
// We already know only non-null ptrs are pushed to self.pages
310+
let addr: std::ptr::NonNull<std::ffi::c_void> =
311+
std::ptr::NonNull::new_unchecked(pg.cast());
312+
mman::mprotect(addr, self.page_size, prot)?;
313+
}
314+
}
315+
for &(hpg, size) in &self.huge_ptrs {
316+
unsafe {
317+
let addr = std::ptr::NonNull::new_unchecked(hpg.cast());
318+
mman::mprotect(addr, size.next_multiple_of(self.page_size), prot)?;
319+
}
320+
}
321+
Ok(())
322+
}
281323
}
282324

283325
#[cfg(test)]

src/shims/native_lib.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
229229
.collect::<Vec<libffi::high::Arg<'_>>>();
230230

231231
// Call the function and store output, depending on return type in the function signature.
232-
let (ret, _) = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
232+
let (ret, maybe_memevents) =
233+
this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
234+
235+
#[cfg(target_os = "linux")]
236+
if let Some(events) = maybe_memevents {
237+
trace!("Registered FFI events:\n{events:#0x?}");
238+
}
239+
#[cfg(not(target_os = "linux"))]
240+
let _ = maybe_memevents; // Suppress the unused warning.
233241

234242
this.write_immediate(*ret, dest)?;
235243
interp_ok(true)
@@ -250,15 +258,15 @@ unsafe fn do_native_call<T: libffi::high::CType>(
250258

251259
unsafe {
252260
if let Some(alloc) = alloc {
253-
// SAFETY: We don't touch the machine memory past this point
261+
// SAFETY: We don't touch the machine memory past this point.
254262
let (guard, stack_ptr) = Supervisor::start_ffi(alloc.clone());
255-
// SAFETY: Upheld by caller
263+
// SAFETY: Upheld by caller.
256264
let ret = ffi::call(ptr, args);
257265
// SAFETY: We got the guard and stack pointer from start_ffi, and
258-
// the allocator is the same
266+
// the allocator is the same.
259267
(ret, Supervisor::end_ffi(guard, alloc, stack_ptr))
260268
} else {
261-
// SAFETY: Upheld by caller
269+
// SAFETY: Upheld by caller.
262270
(ffi::call(ptr, args), None)
263271
}
264272
}

src/shims/trace/child.rs

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,40 @@ impl Supervisor {
5252
// If the supervisor is not initialised for whatever reason, fast-fail.
5353
// This might be desired behaviour, as even on platforms where ptracing
5454
// is not implemented it enables us to enforce that only one FFI call
55-
// happens at a time
55+
// happens at a time.
5656
let Some(sv) = sv_guard.take() else {
5757
return (sv_guard, None);
5858
};
5959

6060
// Get pointers to all the pages the supervisor must allow accesses in
61-
// and prepare the fake stack
61+
// and prepare the fake stack.
6262
let page_ptrs = alloc.borrow().pages();
6363
let raw_stack_ptr: *mut [u8; FAKE_STACK_SIZE] =
6464
Box::leak(Box::new([0u8; FAKE_STACK_SIZE])).as_mut_ptr().cast();
6565
let stack_ptr = raw_stack_ptr.expose_provenance();
6666
let start_info = StartFfiInfo { page_ptrs, stack_ptr };
6767

68+
// SAFETY: We do not access machine memory past this point until the
69+
// supervisor is ready to allow it.
70+
unsafe {
71+
if alloc.borrow_mut().prepare_ffi().is_err() {
72+
// Don't mess up unwinding by maybe leaving the memory partly protected
73+
alloc.borrow_mut().unprep_ffi();
74+
panic!("Cannot protect memory for FFI call!");
75+
}
76+
}
77+
6878
// Send over the info.
6979
// NB: if we do not wait to receive a blank confirmation response, it is
7080
// possible that the supervisor is alerted of the SIGSTOP *before* it has
7181
// actually received the start_info, thus deadlocking! This way, we can
72-
// enforce an ordering for these events
82+
// enforce an ordering for these events.
7383
sv.message_tx.send(TraceRequest::StartFfi(start_info)).unwrap();
7484
sv.confirm_rx.recv().unwrap();
7585
*sv_guard = Some(sv);
7686
// We need to be stopped for the supervisor to be able to make certain
7787
// modifications to our memory - simply waiting on the recv() doesn't
78-
// count
88+
// count.
7989
signal::raise(signal::SIGSTOP).unwrap();
8090
(sv_guard, Some(raw_stack_ptr))
8191
}
@@ -90,7 +100,7 @@ impl Supervisor {
90100
/// one passed to it also.
91101
pub unsafe fn end_ffi(
92102
mut sv_guard: std::sync::MutexGuard<'static, Option<Supervisor>>,
93-
_alloc: Rc<RefCell<IsolatedAlloc>>,
103+
alloc: Rc<RefCell<IsolatedAlloc>>,
94104
raw_stack_ptr: Option<*mut [u8; FAKE_STACK_SIZE]>,
95105
) -> Option<MemEvents> {
96106
// We can't use IPC channels here to signal that FFI mode has ended,
@@ -99,19 +109,22 @@ impl Supervisor {
99109
// simpler and more robust to simply use the signals which are left for
100110
// arbitrary usage. Since this will block until we are continued by the
101111
// supervisor, we can assume past this point that everything is back to
102-
// normal
112+
// normal.
103113
signal::raise(signal::SIGUSR1).unwrap();
104114

115+
// This is safe! It just sets memory to normal expected permissions.
116+
alloc.borrow_mut().unprep_ffi();
117+
105118
// If this is `None`, then `raw_stack_ptr` is None and does not need to
106119
// be deallocated (and there's no need to worry about the guard, since
107-
// it contains nothing)
120+
// it contains nothing).
108121
let sv = sv_guard.take()?;
109122
// SAFETY: Caller upholds that this pointer was allocated as a box with
110-
// this type
123+
// this type.
111124
unsafe {
112125
drop(Box::from_raw(raw_stack_ptr.unwrap()));
113126
}
114-
// On the off-chance something really weird happens, don't block forever
127+
// On the off-chance something really weird happens, don't block forever.
115128
let ret = sv
116129
.event_rx
117130
.try_recv_timeout(std::time::Duration::from_secs(5))
@@ -138,71 +151,71 @@ impl Supervisor {
138151
/// The invariants for `fork()` must be upheld by the caller.
139152
pub unsafe fn init_sv() -> Result<(), SvInitError> {
140153
// FIXME: Much of this could be reimplemented via the mitosis crate if we upstream the
141-
// relevant missing bits
154+
// relevant missing bits.
142155

143156
// On Linux, this will check whether ptrace is fully disabled by the Yama module.
144157
// If Yama isn't running or we're not on Linux, we'll still error later, but
145-
// this saves a very expensive fork call
158+
// this saves a very expensive fork call.
146159
let ptrace_status = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope");
147160
if let Ok(stat) = ptrace_status {
148161
if let Some(stat) = stat.chars().next() {
149-
// Fast-error if ptrace is fully disabled on the system
162+
// Fast-error if ptrace is fully disabled on the system.
150163
if stat == '3' {
151164
return Err(SvInitError);
152165
}
153166
}
154167
}
155168

156-
// Initialise the supervisor if it isn't already, placing it into SUPERVISOR
169+
// Initialise the supervisor if it isn't already, placing it into SUPERVISOR.
157170
let mut lock = SUPERVISOR.lock().unwrap();
158171
if lock.is_some() {
159172
return Ok(());
160173
}
161174

162-
// Prepare the IPC channels we need
175+
// Prepare the IPC channels we need.
163176
let (message_tx, message_rx) = ipc::channel().unwrap();
164177
let (confirm_tx, confirm_rx) = ipc::channel().unwrap();
165178
let (event_tx, event_rx) = ipc::channel().unwrap();
166-
// SAFETY: Calling sysconf(_SC_PAGESIZE) is always safe and cannot error
179+
// SAFETY: Calling sysconf(_SC_PAGESIZE) is always safe and cannot error.
167180
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.try_into().unwrap();
181+
super::parent::PAGE_SIZE.store(page_size, std::sync::atomic::Ordering::Relaxed);
168182

169183
unsafe {
170184
// TODO: Maybe use clone3() instead for better signalling of when the child exits?
171185
// SAFETY: Caller upholds that only one thread exists.
172186
match unistd::fork().unwrap() {
173187
unistd::ForkResult::Parent { child } => {
174188
// If somehow another thread does exist, prevent it from accessing the lock
175-
// and thus breaking our safety invariants
189+
// and thus breaking our safety invariants.
176190
std::mem::forget(lock);
177191
// The child process is free to unwind, so we won't to avoid doubly freeing
178-
// system resources
192+
// system resources.
179193
let init = std::panic::catch_unwind(|| {
180194
let listener =
181195
ChildListener { message_rx, attached: false, override_retcode: None };
182-
// Trace as many things as possible, to be able to handle them as needed
196+
// Trace as many things as possible, to be able to handle them as needed.
183197
let options = ptrace::Options::PTRACE_O_TRACESYSGOOD
184198
| ptrace::Options::PTRACE_O_TRACECLONE
185199
| ptrace::Options::PTRACE_O_TRACEFORK;
186-
// Attach to the child process without stopping it
200+
// Attach to the child process without stopping it.
187201
match ptrace::seize(child, options) {
188202
// Ptrace works :D
189203
Ok(_) => {
190-
let code = sv_loop(listener, child, event_tx, confirm_tx, page_size)
191-
.unwrap_err();
204+
let code = sv_loop(listener, child, event_tx, confirm_tx).unwrap_err();
192205
// If a return code of 0 is not explicitly given, assume something went
193-
// wrong and return 1
206+
// wrong and return 1.
194207
std::process::exit(code.unwrap_or(1))
195208
}
196-
// Ptrace does not work and we failed to catch that
209+
// Ptrace does not work and we failed to catch that.
197210
Err(_) => {
198-
// If we can't ptrace, Miri continues being the parent
211+
// If we can't ptrace, Miri continues being the parent.
199212
signal::kill(child, signal::SIGKILL).unwrap();
200213
SvInitError
201214
}
202215
}
203216
});
204217
match init {
205-
// The "Ok" case means that we couldn't ptrace
218+
// The "Ok" case means that we couldn't ptrace.
206219
Ok(e) => return Err(e),
207220
Err(p) => {
208221
eprintln!("Supervisor process panicked!\n{p:?}");
@@ -212,12 +225,12 @@ pub unsafe fn init_sv() -> Result<(), SvInitError> {
212225
}
213226
unistd::ForkResult::Child => {
214227
// Make sure we never get orphaned and stuck in SIGSTOP or similar
215-
// SAFETY: prctl PR_SET_PDEATHSIG is always safe to call
228+
// SAFETY: prctl PR_SET_PDEATHSIG is always safe to call.
216229
let ret = libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
217230
assert_eq!(ret, 0);
218231
// First make sure the parent succeeded with ptracing us!
219232
signal::raise(signal::SIGSTOP).unwrap();
220-
// If we're the child process, save the supervisor info
233+
// If we're the child process, save the supervisor info.
221234
*lock = Some(Supervisor { message_tx, confirm_rx, event_rx });
222235
}
223236
}

0 commit comments

Comments
 (0)