On RFC-1236, behind the scenes of `catch_unwind`

Maybe zulip isn't quite the right place for asking generic questions so I'm also posting here, too.

Per RFC-1236:

  1. Single-threaded types with interior mutability, such as RefCell, allow for sharing data across stack frames such that a broken invariant could eventually be observed.
  2. Whenever a thread panics, the destructors for its stack variables will be run as the thread unwinds. Destructors may have access to data which was also accessible lower on the stack (such as through RefCell or Rc) which has a broken invariant, and the destructor may then witness this.

My confusion is that why are Rc and RefCell explicitly mentioned here? References point to something in the caller (or ancestor callers), i.e. those on the lower part of the call stack, all the time, no? So why should destructors in the unwind code care about the difference between those data behind generic references and those behind types with interior mutability?

I think the context required is in this issue, which has remarks such as

References are explicitely forbidden because the closure is required to be Send and 'static


The issue and RFC are from 10 years ago; I'm not sure how much UB was being directly enabled in that context. My more contemporary understanding is that the "exception safety" mechanisms we do have are just speed bumps that can't lead to UB without there also still being unsafe elsewhere, which is why you don't need unsafe to disable the mechanisms (AssertUnwindSafe, PoisonError::into_inner). I.e. they're aimed at logical invariant violations heuristically, not at UB directly.[1]

You may wish to contrast the context then with the conversation in this PR and this APC.


  1. And thus the "safetly" verbiage doesn't quite mesh with the usual Rust usages. ↩︎

1 Like

Thx for your context. I'm trying to digest these...

I think I’m mostly on track now: exception safety is still a thing in Rust; it’s the std library design that makes coding with least resistance automatically safe: panicking is arguably more preferable than dealing with data with potentially broken logical (at times safety, if some unsafe were involved) invariant. One example is thread::{scoped, JoinGuard} before 1.9.0. Nowadays we have std::thread::{scope, ScopedJoinHandle} by default panicking unless programmer explicitly asks to ScopedJoinHandle::join, just like std::sync::PoisonError::into_inner.

So I guess the bullet points in the excerpt from the RFC-1236 focuses on what types of variables require extra care when shared with the closure passed to catch_unwind: if the closure were to panic, then those variables are as if one had wrote try {…} catch {…} and thus need to take possibly broken invariant into account, be it logical or safety (if the closure called some unsafe block).

And since Rust soundness is tied to unsafe, I can see why std::sync::PoisonError::into_error and std::thread::ScopedJoinHandle::join are safe functions, and in the same vein why UnwindSafe is safe to implement: logical errors are not soundness problems unless there are unsafe blocks, and when that happens it’s in general the unsafe block’s duty to make itself robust against exception/panic.

Continuing this train of thought, however, I can’t wrap my head around why the sync_nonpoison APC seemed to had gained some decent traction. Yes, one may argue that Mutex is for mutual exclusion and Poison is for dealing with exception safety and these ideas are essentially orthogonal/independent ideas, but the fact two ideas being conceptually orthogonal doesn’t mean they should manifest as two separate APIs: since one cannot reliably detect panic without poisonning, so the fact that with the APC one day the Rust std may migrate to a Mutex implementation that by default doesn’t come with any poison seems daunting to me: when the Mutex poisoning survey launched in 2020 Q4, the top Reddit comment is essentially the same idea, i.e. having a safety-first (not exactly soundness, as discussed above around UnwindSafe, etc, but generic exception safety; same idea dates back to #20807) API is a huge plus for the language; faster, more precise APIs are welcomed, but they should be more verbose to write and not the default, due to its implications… Especially since glancing over reasons as to why poisoning Mutex should not have been the default, it’s mostly about how unwrap everywhere makes a mess: verbosity is good if it’s reminding us something one might had overlooked… just my 2c.

Or maybe I haven’t dug deep enough into the APC…