Maybe zulip isn't quite the right place for asking generic questions so I'm also posting here, too.
Per RFC-1236:
- 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.
- 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.
You may wish to contrast the context then with the conversation in this PR and this APC.
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…