Skip to content

Commit bdf825e

Browse files
committed
fix(port_std): re-implement exit_thread by something similar to longjmp
`pthread_exit` can't unwind through `catch_unwind` anymore because of [1]. Because the actual `longjmp` isn't supported by Rust at this time and [3] is tricky to set up, this commit implements something similar using inline assembler. [1]: rust-lang/rust#70212 [2]: rust-lang/rfcs#2625 [3]: https://github.com/jeff-davis/setjmp.rs
1 parent b7f5042 commit bdf825e

File tree

2 files changed

+143
-8
lines changed

2 files changed

+143
-8
lines changed

src/constance_port_std/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#![feature(thread_local)]
33
#![feature(external_doc)]
44
#![feature(deadline_api)]
5+
#![feature(asm)]
56
#![feature(cfg_target_has_atomic)] // `#[cfg(target_has_atomic_load_store)]`
67
#![feature(unsafe_block_in_unsafe_fn)] // `unsafe fn` doesn't imply `unsafe {}`
78
#![doc(include = "./lib.md")]

src/constance_port_std/src/threading_unix.rs

+142-8
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
//! operation ([`Thread::park`]).
33
use crate::utils::Atomic;
44
use std::{
5+
cell::Cell,
56
mem::MaybeUninit,
67
os::raw::c_int,
7-
ptr::null_mut,
8+
ptr::{null_mut, NonNull},
89
sync::{
910
atomic::{AtomicPtr, AtomicUsize, Ordering},
1011
Arc, Once,
@@ -14,10 +15,15 @@ use std::{
1415

1516
pub use self::thread::ThreadId;
1617

18+
thread_local! {
19+
static EXIT_JMP_BUF: Cell<Option<JmpBuf>> = Cell::new(None);
20+
}
21+
1722
pub unsafe fn exit_thread() -> ! {
18-
unsafe {
19-
libc::pthread_exit(std::ptr::null_mut());
20-
}
23+
let jmp_buf = EXIT_JMP_BUF
24+
.with(|c| c.get())
25+
.expect("this thread wasn't started by `threading::spawn`");
26+
unsafe { longjmp(jmp_buf) };
2127
}
2228

2329
/// [`std::thread::JoinHandle`] with extra functionalities.
@@ -28,7 +34,7 @@ pub struct JoinHandle<T> {
2834
}
2935

3036
/// Spawn a new thread.
31-
pub fn spawn<T: 'static + Send>(f: impl FnOnce() -> T + Send + 'static) -> JoinHandle<T> {
37+
pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle<()> {
3238
let parent_thread = thread::current();
3339

3440
let data = Arc::new(ThreadData::new());
@@ -43,10 +49,14 @@ pub fn spawn<T: 'static + Send>(f: impl FnOnce() -> T + Send + 'static) -> JoinH
4349
// Move `data2` into `THREAD_DATA`
4450
THREAD_DATA.store(Arc::into_raw(data2) as _, Ordering::Relaxed);
4551

46-
parent_thread.unpark();
47-
drop(parent_thread);
52+
catch_longjmp(move |jmp_buf| {
53+
EXIT_JMP_BUF.with(|c| c.set(Some(jmp_buf)));
54+
55+
parent_thread.unpark();
56+
drop(parent_thread);
4857

49-
f()
58+
f()
59+
});
5060
});
5161

5262
let thread = Thread {
@@ -311,6 +321,106 @@ fn ok_or_errno(x: c_int) -> Result<c_int, errno::Errno> {
311321
}
312322
}
313323

324+
#[derive(Copy, Clone)]
325+
#[repr(transparent)]
326+
struct JmpBuf {
327+
sp: NonNull<()>,
328+
}
329+
330+
/// Call `cb`, preserving the current context state in `JmpBuf`, which
331+
/// can be later used by [`longjmp`] to immediately return from this function,
332+
/// bypassing destructors and unwinding mechanisms such as
333+
/// <https://github.com/rust-lang/rust/pull/70212>.
334+
///
335+
/// [The native `setjmp`] isn't supported by Rust at the point of writing.
336+
///
337+
/// [The native `setjmp`]: https://github.com/rust-lang/rfcs/issues/2625
338+
#[inline]
339+
fn catch_longjmp<F: FnOnce(JmpBuf)>(cb: F) {
340+
#[inline(never)] // ensure all caller-saved regs are trash-able
341+
fn catch_longjmp_inner(f: fn(*mut (), JmpBuf), ctx: *mut ()) {
342+
unsafe {
343+
match () {
344+
#[cfg(target_arch = "x86_64")]
345+
() => {
346+
asm!(
347+
"
348+
# push context
349+
push rbp
350+
lea rbx, [rip + 0f]
351+
push rbx
352+
353+
# do f(ctx, jmp_buf)
354+
# [rdi = ctx, rsp = jmp_buf]
355+
mov rsi, rsp
356+
call {f}
357+
358+
jmp 1f
359+
0:
360+
# longjmp called. restore context
361+
mov rbp, [rsp + 8]
362+
363+
1:
364+
# discard context
365+
add rsp, 16
366+
",
367+
f = inlateout(reg) f => _,
368+
inlateout("rdi") ctx => _,
369+
lateout("rsi") _,
370+
// System V ABI callee-saved registers
371+
// (note: Windows uses a different ABI)
372+
out("rbx") _,
373+
lateout("r12") _,
374+
lateout("r13") _,
375+
lateout("r14") _,
376+
lateout("r15") _,
377+
);
378+
}
379+
}
380+
}
381+
}
382+
383+
let mut cb = core::mem::ManuallyDrop::new(cb);
384+
385+
catch_longjmp_inner(
386+
|ctx, jmp_buf| unsafe {
387+
let ctx = (ctx as *mut F).read();
388+
ctx(jmp_buf);
389+
},
390+
(&mut cb) as *mut _ as *mut (),
391+
);
392+
}
393+
394+
/// Return from a call to [`catch_longjmp`] using the preserved context state in
395+
/// `jmp_buf`.
396+
///
397+
/// # Safety
398+
///
399+
/// - This function bypasses all destructor calls that stand between the call
400+
/// site of this function and the call to `catch_longjmp` corresponding to
401+
/// the given `JmpBuf`.
402+
///
403+
/// - The call to `catch_longjmp` corresponding to the given `JmpBuf` should be
404+
/// still active (it must be in the call stack when this function is called).
405+
///
406+
unsafe fn longjmp(jmp_buf: JmpBuf) -> ! {
407+
unsafe {
408+
match () {
409+
#[cfg(target_arch = "x86_64")]
410+
() => {
411+
asm!(
412+
"
413+
mov rsp, {}
414+
jmp [rsp]
415+
",
416+
in(reg) jmp_buf.sp.as_ptr(),
417+
options(noreturn),
418+
);
419+
}
420+
}
421+
}
422+
}
423+
314424
#[cfg(test)]
315425
mod tests {
316426
use super::*;
@@ -352,4 +462,28 @@ mod tests {
352462
// `jh` should be the sole owner of `ThreadData` now
353463
assert_eq!(Arc::strong_count(&jh.thread.data), 1);
354464
}
465+
466+
struct PanicOnDrop;
467+
468+
impl Drop for PanicOnDrop {
469+
fn drop(&mut self) {
470+
unreachable!();
471+
}
472+
}
473+
474+
#[test]
475+
fn test_longjmp() {
476+
let mut buf = 42;
477+
catch_longjmp(|jmp_buf| {
478+
let _hoge = PanicOnDrop;
479+
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| loop {
480+
buf += 1;
481+
if buf == 50 {
482+
unsafe { longjmp(jmp_buf) };
483+
}
484+
}))
485+
.unwrap();
486+
});
487+
assert_eq!(buf, 50);
488+
}
355489
}

0 commit comments

Comments
 (0)