-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deprecating UnwindSafe #3260
base: master
Are you sure you want to change the base?
Deprecating UnwindSafe #3260
Conversation
Thanks for writing this RFC. As it stands, I don't think this RFC has enough detail in it yet. Most of the motivation section, for example, describes things that were explicitly addressed and known about in the RFC that stabilized
In order to deprecate and effectively remove the notion of protecting against exception safety in Rust, I would expect, at minimum, that this RFC provide a comprehenseive argument against it in terms of the RFC that introduced it in the first place. And with respect to the idea that "Unwind safety is not actually related to safety," it's important to note that we were under no illusions about this when we stabilized Responding to the other part of the motivation:
As someone who has been bitten by precisely this, I agree that there is pain here. But what I don't understand is why this means we have to deprecate the notion of exception safety itself. I suppose the obvious question is: how does the pain that library authors feel from this compare with the pain of writing code that isn't exception safe and getting bitten by it? To be fair, that seems like a very hard question to answer. |
One useful piece of data would be what proportion of Whatever the benefit is, it can then be weighed against the cost. If the cost is just having to use From rust-lang/rust#40628, it seems like probably the latter: apparently people who aren't using Another complaint from that thread is that Anyway, it seems like another option alongside or subsuming some of those in the RFC would be "try to fix the problems with
|
It's funny to see this mentioned in TWIR 5 years after I wrote rust-lang/rust#40628 . Personally I don't remember having to fight with unwind safety in these few years, but since nothing changed I still think it's kind of a broken feature. I agree that this RFC could include more content, but generally I agree with the motion. I went through the history of these few slow-paced threads again, and I don't think anyone is all that into It's mostly just a lint. I did it many times in my code, that I though "wouldn't it be nice to have a type system enforcing some logical invariant", introduced it, and then decided that it's not worth the the type-system trouble. Sometimes it works and is worth it, sometimes it isn't. I think here the invariant has to be deal with so rarely in an idiomatic code, that it's just not worth the trouble. There's multiple features that I would rather see Rust language developers spend their precious time on than trying to make this one actually work well, and since a lot of people suggests that we probably want to rename it anyway because it has nothing to do with safety, I think it's easiest to deprecate it completely for now, and if we ever really want to, we can introduce it from scratch, under new name. I guess it wouldn't buy us much if anything to try to fix + rename it, skipping deprecation. |
Why should this logical implication hold? As I understand it, |
If you share an object between threads, you are already crossing unwind boundaries as unwinding stops at thread boundaries. IMO only Sync should imply UnwindSafe, as for Send sharing requires a Mutex, which implements poisoning to cause panics to propagate by default. |
The issue is, currently, neither I do not understand the reasoning for why |
I don't really like that it doesn't implement poisoning. It makes it way to easy to accidentally continue execution when a thread crashed. (Deliberately continuing is fine, but only deliberately)
|
I think I might not have made myself clear. As an exercise, I've made a table with an example of (nearly) every combination of
Perhaps this might help others establish the difference. Personally, I don't see much of a pattern, apart from |
Right, Sync should imply RefUnwindSafe. |
Thank you for raising this point so clearly! Amusingly, when I went back just now to look for the old discussions about making the There are two different kinds of justifications for why a type should be ( The less intuitive one comes from how the types are used, with the observation that using a type in a concurrency-safe way is harder than using it in an unwind-safe1 way, and any code that's concurrency-safe is therefore inherently unwind-safe as well. The But I think the same idea holds true for So in any case where there's an opportunity for From this perspective, the purpose of Now, all this relies on the assumption that concurrency is the only reason why anyone would ever use a (One particular failure mode3 one might be concerned about is that previously, when such types weren't
This is very reasonable. I don't think the tradeoffs are super clear right now w.r.t. how much effort it would take to improve
Removing the Footnotes
|
This reasoning works for atomics, but I don't think it follows for But panics potentially break this last assumption: if a thread panics, it can release its lock before it can restore the invariant, and the next thread to acquire a lock will observe the intermediate state. Thus, we implement poisoning, so that the next thread knows that a panic must have happened in a critical section and that the value must be handled with caution. The alternative is a control-flow explosion, in which every thread to acquire the lock must assume that some other thread may have just panicked.
I have recently observed this while reviewing a library that allows users to write Rust plugins for a certain single-threaded C application. Each plugin has some number of hooks that can be triggered by specific events; the hooks communicate via a captured environment. However, what should happen if one hook panics? There's a few obvious possibilities:
Thus, the author decided to keep all hooks running as before, including the hook that panicked. However, to avoid the control-flow explosion, they required the hook closures to be |
I'm not sure if you were just pointing out a technical detail, or if you intended this as a broader counterargument...? Indeed, while the mutex is locked, its state is protected from other threads, and this critical section can be interrupted by a panic. At that point there are two possibilities. Either it doesn't permit the state to be accessed again (whether by poisoning1, or just keeping it locked), or it does (by unlocking). If it doesn't, the first kind of justification for unwind safety applies -- there's no way to observe a violated invariant. If it does, the second kind does -- now that it's unlocked, another thread can access the state, and this is a bug whether or not
That's an interesting idea, and I guess it makes sense that this use case would come up. Footnotes
|
My apologies; I hadn't seen that you were specifically writing about "the non-poisoning However, there would still seem to be a valuable distinction between poisoning and non-poisoning types. Poisoning types follow our "correct by default" philosophy, since most users aren't expected to carefully track the state of the program following an unwinding panic, and putting up a |
It seems to me that part of the problem may be missing tools to prove the code is actually unwind safe. Further, adding these new types could help: /// Implements poisoning API on top of `&mut` - no shared references involved
pub struct PoisoningCell<T> {
value: T,
is_poisoned: bool,
}
impl<T> PoisoningCell<T> {
pub fn new(value: T) -> Self {
PoisoningCell {
value,
is_poisoned: false,
}
}
pub fn set(&mut self, value: T) {
self.value = value;
// we've just overwrote the value with something valid
self.is_poisoned = false;
}
pub fn replace(&mut self, value: T) -> T { self.ck_poison(); mem::replace(&mut self.value, value) }
pub fn get(&self) -> T where T: Copy { self.ck_poison(); self.value }
pub fn get_ref(&self) -> &T where T: RefUnwindSafe { self.ck_poison(); &self.value }
pub fn get_mut(&mut self) -> Guard<'_, T> { self.ck_poison(); Guard(self) }
pub fn into_inner(self) -> T { self.ck_poison(); self.value }
fn ck_poison(&self) { assert!(!self.is_poisoned, "attempted access to poisoned cell"); }
}
// Consumer can only replace the values or access them with possible poisoning, so these are OK
impl<T> RefUnwindSafe for RefCell<PoisoningCell<T>> {}
impl<T> UnwindSafe for &'_ mut PoisoningCell<T> {}
// impl Deref{Mut} like other guards
pub struct Guard<'a, T>(&'a mut PoisoningCell);
/// Doesn't allow mutable references to T to exist but allows overwriting with new value
pub struct DummyCell<T>(T);
impl<T> DummyCell<T> {
pub fn new(value: T) -> Self { DummyCell(value) }
pub fn set(&mut self, value: T) { self.0 = value; }
pub fn replace(&mut self, value: T) -> T { mem::replace(&mut self.0, value) }
pub fn get(&self) -> T where T: Copy { self.0 }
pub fn get_ref(&self) -> &T where T: RefUnwindSafe { &self.0 }
pub fn into_inner(self) -> T { self.0 }
}
impl<T> RefUnwindSafe for DummyCell<T> {}
// Consumer can only replace the values or read them, so this is OK
impl<T> UnwindSafe for &'_ mut DummyCell<T> {} |
(Sorry for the lag, I had a hard time wrapping my head around the different perspective and got diverted while trying.)
I guess you could look at it this way. If
By using a non-poisoning mutex, someone declares "I take upon myself the burden of ensuring there are no panics [at problematic points] in my critical sections". I agree it would be nice if there were some more upfront indication that this is what's happening (as with As for what *else* could be done about the situation...Effect typing for panics might've come in useful here, but we don't have that. Perhaps the Or maybe the issue in some cases is that the type inside the |
Technically true as long as you only have a single |
@glaebhoerl same applies to |
|
@bjorn3 indeed. There's a footgun that people may forget about it and also the possibility. I'm not sure if it's possible to improve that but maybe some way to mark the invariants would be helpful. |
Yes, because the use case for that is concurrency! I wrote an entire |
I feel like the only goal of unwind safety (being an useful lint) hasn't really been achieved, because there's no consistency in implementing In my opinion, there's no place for half-baked features like this one in std. |
As one of the original people who argued strongly in favor of I'm not 100% sure what the takeaware is here on my part -- I still believe strongly that unwind safety is subtle and hard to get right, and I believe in a std that makes efforts to be footgun-proof against these sorts of failures, but many of the attempts to do so seem to not quite hit the mark (I'm still a fan of lock poisoning, but I know it's unpopular; my personal belief is most code is still broken in the face of panics while a lock is held, and the default API should simply have panicked implicitly rather than returning a |
I disagree, but then I'm one of those people who wraps every unit of work in |
I agree we should provide better tools for catching uncaught panics: but note that, if we do that, and you never have a panic while holding a lock, then poisoning is not something you need to think about. In other words I would rather focus on how we catch the initial panic. Anyway, (mostly) off topic -- but the point stands that having effective mechanisms for preventing panics (and being correct in the face of them) is important. To that end I still believe UnwindSafe was attacking a real problem, but I also see that it seems to be ineffective. I am not entirely sure why that is, as I said.
…On Sat, Oct 28, 2023, at 7:40 AM, Stephan Sokolow wrote:
> (I'm still a fan of lock poisoning, but I know it's unpopular; my personal belief is most code is still broken in the face of panics while a lock is held, and the default API should simply have panicked implicitly rather than returning a Result, and then it would be far more popular).
>
I disagree, but then I'm one of those people who wraps every unit of work in `catch_unwind`, was willing to spawn "unnecessary" threads in the days before `catch_unwind`, sees uncaught panics as something not *that* far below non-FFI use of `unsafe` without miri CI and a giant benchmark suite to justify it as a code smell and constantly laments that Rustig! and findpanics are both unmaintained while `#[no_panic]` is a linker hack with too much emphasis on the "hack".
—
Reply to this email directly, view it on GitHub <#3260 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AABF4ZQW4TZMCKKWHH2BQALYBTVLPAVCNFSM5VA3WZHKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCNZYGM3TQNRUHAYA>.
You are receiving this because you commented.Message ID: ***@***.***>
|
True. I'm just not sure how radical a change it would have involved to developer ergonomics to make the benefit of poisoning negligible. There is, after all, a reason that Rust doesn't require every API which may allocate to return a Panicking, poisoning, |
Maybe it would be reasonable to introduce |
That would mean you can't do any integrity assertions in such functions, which may well be even worse than requiring catch_unwind at a safe place where you can recover from violations of invariants. And it would have all the same composibility issues as any other effect system we have in rust (const fn, async fn, ...) |
@nikomatsakis Do you happen to have any thoughts w.r.t. my earlier comment where I tried to figure out what actual practical problems people are having with UnwindSafe, and whether they could be addressed? (Also maybe this footnote on a subsequent comment, w.r.t. what it would take for the Send/Sync->Ref/UnwindSafe blanket impls to go through.) |
rust-lang/rust#40628, rust-lang/rust#65717 and rust-lang/rfcs#3260 all show that unwind safety isn't particularly ergonomic to use and implement, and ultimately leads to people slapping `AssertUnwindSafe` everywhere until the compiler stops complaining. This situation has led to built-in test framework using `catch_unwind(AssertUnwindSafe(...))` (see https://github.com/rust-lang/rust/blob/1.73.0/library/test/src/lib.rs#L649) and libraries like tower-http doing the same (see https://docs.rs/tower-http/0.4.4/src/tower_http/catch_panic.rs.html#198). As people have mentioned in the threads above, trying to implement this correctly is akin to fighting windmills at the moment. Since the above cases demonstrated that `catch_unwind(AssertUnwindSafe(...))` is currently the easiest way to deal with this situation, this commit does the same and refactors our background job runner code accordingly.
rust-lang/rust#40628, rust-lang/rust#65717 and rust-lang/rfcs#3260 all show that unwind safety isn't particularly ergonomic to use and implement, and ultimately leads to people slapping `AssertUnwindSafe` everywhere until the compiler stops complaining. This situation has led to built-in test framework using `catch_unwind(AssertUnwindSafe(...))` (see https://github.com/rust-lang/rust/blob/1.73.0/library/test/src/lib.rs#L649) and libraries like tower-http doing the same (see https://docs.rs/tower-http/0.4.4/src/tower_http/catch_panic.rs.html#198). As people have mentioned in the threads above, trying to implement this correctly is akin to fighting windmills at the moment. Since the above cases demonstrated that `catch_unwind(AssertUnwindSafe(...))` is currently the easiest way to deal with this situation, this commit does the same and refactors our background job runner code accordingly.
Our invariants are so complicated and need such special handling that once they are take care of, `UnwindSafe` adds nothing. The particular example that motivated this: `&mut T` isn't `UnwindSafe` because it's *easy* to witness a broken invariant, not that you will. See rust-lang/rfcs#3260 for further rationale.
Problems I have with
|
@kornelski How I'd consider it, is that
I'm not really opposed to this PR, since |
This RFC proposes to deprecate UnwindSafe.
Rendered RFC
Pre-RFC discussion on internals