This thread serves as a thread for side-discussions that started in another thread that aims to discuss how good or bad a term the term "object safety" is.
This thread can hold the more in-depth exploration of other usages of the word "safety" in Rust terminology, e.g. where and how and where exactly "safety" is used in similar - or perhaps quite different - ways, which was clarified to be explicitly not a main focus of that other thread:
Since multiple users had already even felt the need to mark clarifications on "what means what" specifically around "panic safety" vs "unwind safety" as "off-topic" themselves, I have moved all those answers here; and the other thread can focus better on ideas around the name of the notion of "object safety" specifically.
Since its conception, Rust has always acknowledged that memory safety is not the only kind of safety that is important.
For example, another important safety property is exception safety per Rustonomicon, borrowing the C++ term, though the stdlib calls it a much better, more accurate term, unwind safety (but the docs also refers to panic safety which means there's 3 expressions for the same thing, which is unfortunate).
Another common programming term, not specific to Rust (but also used in Rust) is thread safety.
Both exception safety and thread safety has a memory safety component (in unsafe code, we can't be so careless as to allow unwinding or concurrency bugs to make the program memory unsafe), but they generally entail something stronger than just memory safety.
(There's also another kind safety, type safety, but it's intertwined with memory safety: Rust guarantees that, as long as you uphold memory safety in unsafe code, your code is type safe as well)
As a rule of thumb, having some kind of "safety" means upholding an important invariant (which are properties that we can rely upon when thinking about the code). Memory safety is very prominent but was never meant to be the only kind of safety that Rust programmers care about; it's just the bare minimum.
UnwindSafe is a great example of abusing the term "safety" IMO. It promises nothing you can rely on; you do not need unsafe to use AssertUnwindSafe to get something that implements the trait without changing anything else. It's just a speed-bump trying to say, apparently, "hey... think about it."
But often there's nothing to think about when it bites (catch_unwind), e.g. you have a generic closure. And so the "fix" is almost always just "slap AssertUnwindSafe on it" without thinking. Even when you are the one who might be able to observe the logic errors it's attempting to lint against, you can't even rely on std types having implementing the traits sensibly. Because these are auto-traits, that also makes it a SemVer hazard. Particular since no-one wants their erased types to look like dyn Trait + UnwindSafe + RefUnwindSafe.
i think there's an argument that panic saftey and unwind saftey could be considered different things, since a panic may not always result in an unwind.
Both "panic safety" and "unwind safety" must refer to the same thing, which is: what happens when we observe data structures after a panic? Are they in a broken state? But this can only happen with panic=unwind.
If you compile with panic=abort you sidestep this whole issue really, because no code runs after a panic. And aborting on panic is arguably the simpler alternative, because this stuff is hard to get right, and the fact that UnwindSafe didn't see much adoption (there's even a pushback against it and other panic safety facilities like lock poisoning) doesn't help at all.
They're not the same thing. "Panic safety" in Rust is generally used as analogous to C++ exception safety, meaning the program must prevent panics from causing any memory unsafety. If code is not "panic safe", it's unsound (and panic=abort is not an excuse for unsoundness, the code must abort itself if that's necessary).
But UnwindSafe is just vague handwaving about some potential logic bugs, which specifically are not memory safety bugs, and not soundness issues defined by Rust. If code is not UnwindSafe, it's just a shrug.
I think UnwindSafe was just an overreaction to introduction of catch_unwind. It was a scary change, because Graydon's original Rust was not supposed to have it.
But in practice the already-well-defined memory safety is the hard problem with unwinding, and programs that fix memory safety bugs have no reason to leave other bugs unfixed. This makes UnwindSafe with no real purpose beyond being a vague reminder to think about unwinding, but even for that role it has too many false positives. It fires not when libraries are authored, but often when downstream users use the libraries, and at that point can't do much about it other than YOLO AssertUnwindSafe.
This is not true. You can observe data structures after their code has called panic!(), without unwinding, provided that those structures are reachable via statics or shared ownership — by installing a panic hook, which is a safe operation. The panic hook executes even with panic=abort — it's what is responsible for printing the panic message.
Therefore, the only actual guarantee you get from panic!() is that it diverges; you can’t rely on it not doing anything in particular.
(I have a suspicion that there is lots of code that is unsound by this route, but I haven’t collected any actual data on it.)
I'm not sure what you mean by this: are you saying the general issue of library code that assumes panic=unwind? Or something more specific about reachability of values after a panic?
mod some_library {
use std::cell::UnsafeCell;
thread_local! {
static STATE: UnsafeCell<Vec<u8>> = UnsafeCell::new(vec![0; 10]);
}
pub fn foo(index: usize, value: u8) {
STATE.with(|cell: &UnsafeCell<Vec<u8>>| {
// Safety:
// `foo()` takes no user-supplied types or functions,
// and STATE is thread-local,
// so there is no way to obtain overlapping mutable references...?
let data: &mut Vec<u8> = unsafe { &mut *cell.get() };
// This panics on out of bounds, but that's okay, right?
// Lots of Rust code has panics on bad input...
data[index] = value;
});
}
}
fn main() {
std::panic::set_hook(Box::new(|_| {
eprintln!("panicking");
// Oops.
some_library::foo(0, 1);
}));
some_library::foo(9999, 9);
}
This illustrates the necessary elements, but real cases could have much more incidental complexity making it harder to see that
foois potentially reentered; the safety argument is incorrect, and
the shared data is borrowed or written in an overlapping fashion.
Oof, that's a real nasty one! Without unsafe being in the language, it would be real easy to just say "don't call this function in a panic hook, I never promised re-entrancy", but it's still really hard to blame the poor dev that used unsafe here...
Another "fun" occurrence of this same kind of hidden reentrancy is through the allocator. I don't really know which is "worse" per say — whether and how a bit of code panics is supposed to be documented whereas allocation is more acceptable to introduce than a use of encapsulated unwinding, but the language does actually provide a way to write alloc-free code a lot more practically than it does panic-free code.
This pattern has been understood a lot longer with interrupts as the source of "surprise control flow," and there it's fairly clear that the solution is that interrupts morally occur on a separate logical thread even if they're on the same physical thread as the one that they're interrupting. The #[global_allocator] static and the panic hook callback are required to be Sync, so I think the resolution is similar here — consider the hooks to be morally a separate thread.
It does mean that code can't rely on freedom from reentrancy except in highly constrained scenarios (it holds &mut to some unique token), but I think the scenarios where this is necessary should be fairly minimal. Although cases where it happens despite not technically being a valid assumption are certainly more prevalent than anyone would like.
What does "considering the panic hook to be a morally a separate thread" imply in this context? Adding some sort of "we're in scary language hook code right now" that things like thread_local can check? Or just a general reminder to developers that any code that can panic or allocate (or...?) should be in the same bucket as threading?
Mostly just as a reminder to developers. If we had "task local" contexts instead of "thread local" then it would make sense to present these hooks as running in a different context, but since Rust directly uses the OS concept of a thread, disguising that fact only makes things worse.
Panic/alloc hook are special in that they're extremely ubiquitous. They are "just" callbacks, but thinking about them in terms of a separate threading context for safety purposes (although running on the same physically identified thread) is a strong contender for the "best" way to conceptualize it, due to how often they get the opportunity to run.
Interrupts are more special and need to be a separate logical thread on the AM for the purpose of racing, where the hooks do just run in the context that calls them. Thinking about them like threading is only an affordance to remembering that they can happen at basically any point as far as soundness is concerned.
(And actually, at least one of the commonly proposed models for handling allocation allows insertion of spurious allocation events. I currently expect Rust will use something stricter than that, but that's what "allocation is unobservable" gets you. Allocation in the same AM is a tricky problem.)