std::threads: revisit stack address calculation on netbsd. by devnexen · Pull Request #122002 · rust-lang/rust (original) (raw)
Wow that's a lot of hops to trace this logic.
OK, cool, thanks for tracking this down!
Is there another call for threads created with pthread_create, or is it only called for the main thread?
If the code you quoted covers all uses of the outputs, then it looks like this logic is used only to determine a region of memory in which a SIGSEGV probably indicates a stack overflow.
Almost. The sys::thread:💂:init
fn is only called on the first-time startup there, which does whatever guard mapping it needs. There is one other place that calls thread_info::set
, which takes the stack guard argument, and it is thread::Builder::spawn_unchecked
:
unsafe fn spawn_unchecked_<'a, 'scope, F, T>( |
---|
self, |
f: F, |
scope_data: Option<Arcscoped::ScopeData\>, |
) -> io::Result<JoinInner<'scope, T>> |
where |
F: FnOnce() -> T, |
F: Send + 'a, |
T: Send + 'a, |
'scope: 'a, |
{ |
let Builder { name, stack_size } = self; |
let stack_size = stack_size.unwrap_or_else(thread::min_stack); |
let my_thread = Thread::new(name.map(|name |
CString::new(name).expect("thread name may not contain interior null bytes") |
})); |
let their_thread = my_thread.clone(); |
let my_packet: Arc<Packet<'scope, T>> = Arc::new(Packet { |
scope: scope_data, |
result: UnsafeCell::new(None), |
_marker: PhantomData, |
}); |
let their_packet = my_packet.clone(); |
let output_capture = crate::io::set_output_capture(None); |
crate::io::set_output_capture(output_capture.clone()); |
// Pass `f` in `MaybeUninit` because actually that closure might *run longer than the lifetime of `F`*. |
// See https://github.com/rust-lang/rust/issues/101983 for more details. |
// To prevent leaks we use a wrapper that drops its contents. |
#[repr(transparent)] |
struct MaybeDangling<T>(mem::MaybeUninit<T>); |
impl<T> MaybeDangling<T> { |
fn new(x: T) -> Self { |
MaybeDangling(mem::MaybeUninit::new(x)) |
} |
fn into_inner(self) -> T { |
// SAFETY: we are always initialized. |
let ret = unsafe { self.0.assume_init_read() }; |
// Make sure we don't drop. |
mem::forget(self); |
ret |
} |
} |
impl<T> Drop for MaybeDangling<T> { |
fn drop(&mut self) { |
// SAFETY: we are always initialized. |
unsafe { self.0.assume_init_drop() }; |
} |
} |
let f = MaybeDangling::new(f); |
let main = move | |
if let Some(name) = their_thread.cname() { |
imp::Thread::set_name(name); |
} |
crate::io::set_output_capture(output_capture); |
// SAFETY: we constructed `f` initialized. |
let f = f.into_inner(); |
// SAFETY: the stack guard passed is the one for the current thread. |
// This means the current thread's stack and the new thread's stack |
// are properly set and protected from each other. |
thread_info::set(unsafe { imp:💂:current() }, their_thread); |
let try_result = panic::catch_unwind(panic::AssertUnwindSafe(| |
crate::sys_common::backtrace::__rust_begin_short_backtrace(f) |
})); |
// SAFETY: `their_packet` as been built just above and moved by the |
// closure (it is an Arc<...>) and `my_packet` will be stored in the |
// same `JoinInner` as this closure meaning the mutation will be |
// safe (not modify it and affect a value far away). |
unsafe { *their_packet.result.get() = Some(try_result) }; |
// Here `their_packet` gets dropped, and if this is the last `Arc` for that packet that |
// will call `decrement_num_running_threads` and therefore signal that this thread is |
// done. |
drop(their_packet); |
// Here, the lifetime `'a` and even `'scope` can end. `main` keeps running for a bit |
// after that before returning itself. |
}; |
From guard::current()
, which is like this for NetBSD:
#[cfg(any( |
---|
target_os = "android", |
target_os = "freebsd", |
target_os = "hurd", |
target_os = "linux", |
target_os = "netbsd", |
target_os = "l4re" |
))] |
pub unsafe fn current() -> Option<Guard> { |
let mut ret = None; |
let mut attr: libc::pthread_attr_t = crate::mem::zeroed(); |
#[cfg(target_os = "freebsd")] |
assert_eq!(libc::pthread_attr_init(&mut attr), 0); |
#[cfg(target_os = "freebsd")] |
let e = libc::pthread_attr_get_np(libc::pthread_self(), &mut attr); |
#[cfg(not(target_os = "freebsd"))] |
let e = libc::pthread_getattr_np(libc::pthread_self(), &mut attr); |
if e == 0 { |
let mut guardsize = 0; |
assert_eq!(libc::pthread_attr_getguardsize(&attr, &mut guardsize), 0); |
if guardsize == 0 { |
if cfg!(all(target_os = "linux", target_env = "musl")) { |
// musl versions before 1.1.19 always reported guard |
// size obtained from pthread_attr_get_np as zero. |
// Use page size as a fallback. |
guardsize = PAGE_SIZE.load(Ordering::Relaxed); |
} else { |
panic!("there is no guard page"); |
} |
} |
let mut stackptr = crate::ptr::null_mut::libc::c\_void\(); |
let mut size = 0; |
assert_eq!(libc::pthread_attr_getstack(&attr, &mut stackptr, &mut size), 0); |
let stackaddr = stackptr.addr(); |
ret = if cfg!(any(target_os = "freebsd", target_os = "netbsd", target_os = "hurd")) { |
Some(stackaddr - guardsize..stackaddr) |
} else if cfg!(all(target_os = "linux", target_env = "musl")) { |
Some(stackaddr - guardsize..stackaddr) |
} else if cfg!(all(target_os = "linux", any(target_env = "gnu", target_env = "uclibc"))) |
{ |
// glibc used to include the guard area within the stack, as noted in the BUGS |
// section of `man pthread_attr_getguardsize`. This has been corrected starting |
// with glibc 2.27, and in some distro backports, so the guard is now placed at the |
// end (below) the stack. There's no easy way for us to know which we have at |
// runtime, so we'll just match any fault in the range right above or below the |
// stack base to call that fault a stack overflow. |
Some(stackaddr - guardsize..stackaddr + guardsize) |
} else { |
Some(stackaddr..stackaddr + guardsize) |
}; |
} |
if e == 0 | |
assert_eq!(libc::pthread_attr_destroy(&mut attr), 0); |
} |
ret |
} |
} |
We may wish to adjust how we set up threads, in light of this information (this has been quite illuminating, thank you! I wish the man pages for the pthread headers were as clear...) but I believe it's currently harmless, because this information still is only consumed by the signal handler.
Side notes about this code:
* You don't need to mmap or mprotect guard pages. NetBSD already does this for you.
Right, it sounds like for sys::thread:💂:init
the OpenBSD branch here is the one that NetBSD should be using? Though it sounds like the stack isn't immutable, exactly, on NetBSD, but we "don't care" because, thinking about it more clearly now, anyone running pthread_attr_setstack
has to be running it after sys::thread:💂:init
, in which case we can't exactly interdict their syscalls, as far as I know, and they're taking their life into their own hands, OR we never get to run this function anyways.
} else if cfg!(target_os = "openbsd") { |
---|
// OpenBSD stack already includes a guard page, and stack is |
// immutable. |
// |
// We'll just note where we expect rlimit to start |
// faulting, so our handler can report "stack overflow", and |
// trust that the kernel's own stack guard will work. |
let stackptr = get_stack_start_aligned()?; |
let stackaddr = stackptr.addr(); |
Some(stackaddr - page_size..stackaddr) |