You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Edition 2027: Consider replacing std::sync::Mutex and other locks with std::sync::nonpoison versions #149359
This serves as a place to discuss changing default lock types to their nonpoison versions, which has come up a number of times before but not yet (to my knowledge) discussed in depth.
@davepacheco brings up a good point at #134645 (comment) that having posioning locks tends to be a safer default, and that the .unwrap() annoyance could be smoothed with panicking methods rather than changing out the type.
I'm not sure whether or not an RFC would be required but considering the impact, it doesn't seem like a bad idea (see also rust-lang/rfcs#3550 for a similar change, though that involved lang).
Reacted by Eliza Weisman, Juhan Oskar Hennoste, Sherlock Holo, Thomas Dagenais, Connor Tsui, caelunshun, ahoyiski, Joey de Waal, Gregory Petrosyan, Rostyslav Lesovyi and 11 more
Reacted by bjorn3, tmpolaczyk, Aapo Alasuutari, Vincent Esche, Emanuele Stoppa, Erich Gubler, Diana, Sandro Stikić, JungChul Shin, Dean Serenevy and 6 more
Reacted by Juhan Oskar Hennoste, may, Redddy, Cathal, gulbanana, oxalica, Michael Yang, qouteall and foob2483-oss
One adjacent topic that IMO is worth considering is panic=abort. I work on a codebase that's largely compiled with panic=abort so the poisoning API doesn't offer any value to those programs. Given https://smallcultfollowing.com/babysteps/blog/2024/05/02/unwind-considered-harmful/ and other discussions I wonder whether it's worth considering the frequency of poisoned locks even being observable in users' programs in practice.
Reacted by Jiahao XU, matthieu-m, Rostyslav Lesovyi, Bryan Hitchcock, Armin, qouteall and Matt Johnston
and that the .unwrap() annoyance could be smoothed with panicking methods rather than changing out the type.
I would be 100% in favor of this! If anything I did rather make .lock() panic on poison by default (with a different lock method returning a result that you can handle) than remove poison from the default mutex type. Without poisoning you are way too likely to keep limping around without making any forward progress ever again when an essential background thread panicked.
Reacted by Jonas Platte, Eliza Weisman, iximeow, Jiahao XU, oxalica, matthieu-m, Nick Cameron, Diana, Greg Morenz, chc4 and 22 more
As a quick example, here's code which would panic with the current behavior, but would be UB (dereferencing a null pointer) with non-poisoning mutexes:
use std::sync::Mutex;/// The mutex protects the invariant that the pointer is always validstructPointerWrapper(Mutex<*mutu32>);constSHOULD_PANIC:bool = false;fnpoke_pointer(p:&PointerWrapper){letmut p = p.0.lock().unwrap();// Briefly break invariants to manipulate the pointerlet prev = std::mem::replace(&mut*p, std::ptr::null_mut());let next = do_something_that_could_panic(prev);*p = next;}fndo_something_that_could_panic(ptr:*mutu32) -> *mutu32{ifSHOULD_PANIC{panic!("oh no");}
ptr
}fnmain(){letmut i = 123u32;let p = PointerWrapper(Mutex::new(&mut i));let _ = std::panic::catch_unwind(|| poke_pointer(&p));let value = p.0.lock().unwrap();println!("{}",unsafe{**value });}
This is obviously a contrived example – and one could argue that updating to remove the .unwrap() would force people to consider the edition changes – but in a large codebase, the path of least resistance would be for people to remove
the unwrap() and go along with their day, unaware that the std::sync::Mutex behavior has changed underneath them.
(FWIW, I also like the idea of making .lock() panic on poisoning, which could happen at an edition boundary!)
Reacted by Eliza Weisman, matthieu-m, Diana, Greg Morenz, chc4, Micah Johnston, Jack O'Connor, Maxime Poulin, A.L.I.C.E, Jeff Burdges and 15 more
Reacted by Orson Peters
Reacted by oxalica, David Hoppenbrouwers and Orson Peters
It's worth noting that if lock() is changed to always panic on poisoning at an edition boundary, it should probably only do so if !std::thread::panicking(); I've seen plenty of code where PoisonErrors are always unwrapped except in a Drop impl, to avoid recursive panics causing the program to immediately abort.
One downside of making the API unconditionally panic rather than return a Result is how it effects this case, actually. If poisoning is ignored when the thread is already panicking, then the Drop implementation can witness the potentially-violated invariants. I've seen code where Drop implementations that do something not required for memory safety (i.e. updating timing data or counters or similar that are inside of a lock) just only do that action when lock() returns Ok, and skip it if the lock has already been poisoned and the Drop impl was called in the midst of unwinding. Changing the lock() API to unconditionally panic instead does make that behavior unexpressable.
Reacted by Matt Keeter, zachs18, Jiahao XU, Guillaume Raffin, Diana, Aapo Alasuutari, Vincent Esche, Connor Tsui, Tci Gravifer Fang, qouteall and 2 more
Changing the lock() API to unconditionally panic instead does make that behavior unexpressable.
We should still keep the non-panicking variant of the lock method around IMO. Just with a different name.
Reacted by Zachary Harrold, Greg Morenz, chc4, Jack O'Connor, Sawyer Bergeron, Aapo Alasuutari, Paul Daniel Faria, Vincent Esche, David Wolff, Haoxuan and 7 more
Without poisoning you are way too likely to keep limping around without making any forward progress ever again when an essential background thread panicked.
I've also heard of (haven't personally seen) situations where poisoning does the opposite, putting the program into a permanent bad state when it would've been fine otherwise. Consider a service that spawns a thread for each connection and never joins them. If one request panics while it happens to hold an important lock, poisoning could cause ~all requests after that to panic. Then the service would be failing all requests until some watchdog happened to notice and reboot it. I think @mitsuhiko had a story about this somewhere.
Reacted by Juhan Oskar Hennoste, Jiahao XU, Connor Tsui, Rostyslav Lesovyi, Josh Triplett, Alisa Sireneva, zachs18, Armin Ronacher, qouteall and Matthew Brown
Also I noticed that the current RefCell impl, which is akin to a single-threaded Mutex1, does not have poisoning, thus cannot "protect" user from forgetting unwinding exists during temporary invariant violation. I have not found any user who complains about that, which is definitely less than the number of users complaining about poison-by-default behavior of mutex.
As a quick example, here's code which would panic with the current behavior, but would be UB (dereferencing a null pointer) with non-poisoning mutexes:
It's not "would be" but "is already" a UB. It is mentioned in #143471 that the poison behavior is on best-effort basis and must NOT be relied for safety purpose. To prove it's unsound, we can practically make it segfault from a totally safe caller, by using a similar method given in that issue: playground.
Reacted by Juhan Oskar Hennoste, zachs18, Rostyslav Lesovyi, Slanterns, Armin Ronacher, Alisa Sireneva, Connor Tsui, Daniel Hollas, Michael Yang, LIParadise and 3 more
This would be a very significant, semver-breaking change. You are also underestimating the complexity: if you want to keep scoped threads or catch_unwind at least in some way, you need effects, which is non-trivial to integrate into an existing language.
Why? It seems me almost all Rust code already doesn't break unwind safety, and semver-breaking can be avoided by introducing new methods and deprecating older ones. catch_unwind is already sound if AssertUnwindSafe is not used, and scoped threads seem easily fixed with panic propagation from children threads to the parent thread at scope closure (which is a pretty intuitive thing to happen for a scoped thread).
Why do you think this is impossible and an effect system is needed?
This is somewhat off-topic for this thread, so I'll keep my response brief. The main issue is that the UnwindSafe trait tracks whether a type is unwind-safe, but we're interested in is whether operations we perform are unwind-safe. This has multiple implications:
You can have multiple mutating methods, where some objects are guaranteed not to panic (and should thus be usable in catch_unwind/whatever), or perhaps handle panics gracefully, while others may break invariants on panic. You cannot currently do this on a per-function granularity, only on a per-type granularity, which would be akin to forcing all methods to consistently take either &mut self or &self.
UnwindSafe is derived for closures based on their captures, since closures are effectively just structs. Yet closures can access non-unwind-safe objects not through captures: they can access thread-locals or receive mutable references through channels. Forcing all thread locals to be UnwindSafe is not an option, making Receiver<T>: UnwindSafe require T: UnwindSafe would be weird because the channel itself handles panics correctly, and it's entirely sound to have a thread that, say, reads from Receiver<T> and writes to Sender<T> in a loop. There's probably more real-world examples I'm missing.
Type-granular unwind safety is not well-defined. Logically, primitives should not have any associated invariants: only user-constructed objects can. Yet while i64 implements UnwindSafe, the primitive &mut T doesn't, and &T only implements it conditionally. There is no way to represent pure data.
UnwindSafe can only track data invariants, not global state invariants: for example, you can't track stuff like locking the file system, since that doesn't access any data type.
IMO, UnwindSafe is fundamentally a category error. Tracking individual operations, i.e. introducing effects, would solve a lot of these problems. Of course, this is not a realistic approach, since it relies either on global inference, or on explicit annotations in all user code that introduces invariants, none of which is reasonable to do in a mature language.
The fact that, as you say, "all Rust code doesn't break unwind safety" is, in my opinion, an implication of a particular combination of false positives and false negatives. &mut T being non-unwind-safe, means that all realistic Rust code does access non-unwind-safe objects, but the fact that UnwindSafe is only checked for captures and not internally accessed objects means that this is seldom noticed.
Poisoning mutexes really have the same issues. You can't say that panic during method A should poison the mutex, while panic during method B shouldn't. You can't use Mutex if data is shared in some other way, say via RefCell or atomics (though I can't imagine how Poison could possibly work with atomics). Adding non-poisoning mutexes arguably resolves the issue with non-well-defined unwind safety simply by making two types, though you still won't be able to store Mutex in pure data and let the consumer decide whether it can violate invariants (though mutexes are seldom stored in data, so this is not a practical worry). Poisoning can be opted into for global state invariants with Mutex<()>, but it's not the default and you have to remember to do it. (Though mutex-only poisoning has another problem, which is that it conflates mutability and invariant violation, while you can clearly violate invariants in code using, say, atomics.)
I got curious when noticing that nonpoison::Mutex was a thing, and even more so when it required multiple feature flags to even use. It led me down these threads, so:
As of right now sync::nonpoison::Mutex isn't even stable, so I suspect most people are currently using the poisoning mutex simply because it is the only option in the standard library on stable Rust. I don't think it's possible to make these kinds of conclusions just yet.
Is very true.
There is a lot of stigma around using unwrap(), and consequently I find myself mildly disgusted by using it in "fine if you know what you're doing" sorta places. Or then you could spend trice as long error handling what would have worked fine.
At any rate, I don't see how the nonpoison API could be opposed much. If it's very common see lock().unwrap() without much afterthought, then clearly it's the more intuitive option most the time even besides the high-performance aspect. Especially when panics aren't even a concern in the first place. As a contrived example: Assigning statistics to a dedicated tracker at the end of a one-shot async move (totally not what let me to finding nonpoison in the first place). But anyway, I just found the discussion intriguing, not that I am versed in Rust internals.
At any rate, I don't see how the nonpoison API could be opposed much. If it's very common see lock().unwrap() without much afterthought, then clearly it's the more intuitive option most the time even besides the high-performance aspect. Especially when panics aren't even a concern in the first place.
You seem to be implying nonpoison and lock().unwrap() are equivalent, when they're very much not. If a thread holding a mutex panics, non-poisoning mutexes will silently unlock, while poisoning mutexes will panic when lock().unwrap() is called.
Reacted by Matt Keeter, LIParadise and Dante Broggi
I've been mulling over mutexes and our plans for 202x and I think there's simpler ways to proceed here which let us (mostly) have our cake and eat it too. It seems to me like a lot of the pain from 2024 mutexes does ultimately come from ".lock().unwrap() is annoying", which i'm entirely sympathetic to. I also think that unwind safety in general is something that needs a lot more attention (cough allocator_api) and the mechanisms we provide right now are generally insufficient. However, mutex poisoning is probably one of the few ways in which we do actually steer code towards being resilient against unwind-unsafety. If even std is susceptible to unwind-unsafety bugs in paths that didn't unwind previously, I don't think I buy the argument that's been made before that it's all up to unsafe code to be resilient. Sure, it should be, but let's not cause extra unsoundness unnecessarily and instead make it easy to do the correct thing.
We could keep poisoning default and just make the ergonomics nicer; edition-dependent name resolution is already going to be necessary for the current proposal, so why not do it for methods instead? sync::poison::Mutex can stay the default, with .lock() becoming equivalent to .lock().unwrap() and a separate .lock_checked() method. If we want to, we could expose the new semantics as .lock_forced() on <=2024 as well. This has the bonus advantage that swapping between poisoning and nonpoisoning mutexes becomes easier for implementors, since they'd expose the same API surface (except for panicking, ofc); just change the use statement.
(I realise this is not necessarily a new argument, but I would hope the points of evidence re: recent unsoundesses in std are relevant)
Reacted by Tyler Mandry and Andy Barron
Reacted by bjorn3, Hanna Kruppe, may, Jacob Greenfield, Connor Tsui, Diana, Matt Keeter, David Pacheco and Rain
with .lock() becoming equivalent to .lock().unwrap()
I don't really like hiding unwraps. At least currently it is explicit and shows that something might go wrong. IMHO new developers (or anyone who is not aware of poisoning in general) will rarely read documentation about the Mutex, it just simply such a basic primitive available in most languages.
If Cloudflare incident has showed us anything - is that even unwrap is not explicit enough for poisoning and can be ignored in relatively important infrastructure.
The Cloudflare incident had nothing to do with poisoning at all. And I even disagree that the .unwrap() was really at fault there. Remember that the non-Rust version of the same code put everyone behind an infinite captcha, which is worse than a honest 500 error message as it wastes your time. The root cause was garbage data being fed into the server process that the server process wasn't able to handle correctly. If anything poisoning reduces the risk of such garbage data propagating out and into another thread that can handle the garbage even less.
Reacted by Connor Tsui, Nia Deckers, Diana, Slanterns, David Pacheco, blep and foob2483-oss
with .lock() becoming equivalent to .lock().unwrap()
That would endorse .unwrap() as the correct default behavior for poisoning locks. While unwrapping is common I would argue that it is incorrect in most cases and only works out because the unhappy path where the lock is poisoned is extremely rare in practice.
While unwrapping is common I would argue that it is incorrect in most cases and only works out because the unhappy path where the lock is poisoned is extremely rare in practice.
Panicking on poisoned mutexes is more correct than silently working on corrupted data (which is what std::sync::nonpoison would do), but can be less correct than explicitly handling the poisoned data. So yes, panicking is a convenience tradeoff, but it is a better one that operating on corrupted data like std::sync::nonpoison would do.
Reacted by Nia Deckers, Jonas Platte, Connor Tsui, James C. Wise, Daniel Hollas, Trevor Gross, David Tolnay, Diana, zroug, Jiahao XU and 5 more
I don't really like hiding unwraps. At least currently it is explicit and shows that something might go wrong.
While unwrapping is common I would argue that it is incorrect in most cases [...]
These are reasonable points, but importantly, panicking is a safe and adjustable default. The vast majority of code cannot meaningfully make sense of possibly-broken values (as might be behind a poisoned mutex), and the poisoning of said mutex in and of itself is proof that unexpected panics have already started occuring in a given execution. There are of course going to be exceptions and cases where poison can be recovered from that might be overlooked, but unnecessary panic propagation is only a correctness issue; observing a value in an invalid state (especially without knowing it) can trigger a lot of UB
Reacted by Connor Tsui, James C. Wise, Jacob Greenfield, David Pacheco, iximeow, Rain and Tyler Mandry
Another argument for keeping the poisoning mutexes the default is that the Google rust style guide explicitly forbids peeking into poisoned mutexes no matter the use case: https://google.github.io/styleguide/rustguide.html#handling-poison Using nonpoison mutexes by default would make it impossible to enforce that rule as you don't know if a mutex was held while another thread panicked (and thus semantically poisoned) unless a physical poison flag is set.
FWIW I would not recommend using that as justification for this discussion. The style rule was added in a very early phase of designing the style rules and stuck around. It matches a general "don't do sketchy things" policy that is behind those style rules. We could reevaluate it if Rust removed poisoning.
Reacted by Slanterns, Juhan Oskar Hennoste, Marco Groppo and GrigorenkoPV
Activity
anp commentedon Nov 26, 2025
One adjacent topic that IMO is worth considering is
panic=abort. I work on a codebase that's largely compiled withpanic=abortso the poisoning API doesn't offer any value to those programs. Given https://smallcultfollowing.com/babysteps/blog/2024/05/02/unwind-considered-harmful/ and other discussions I wonder whether it's worth considering the frequency of poisoned locks even being observable in users' programs in practice.bjorn3 commentedon Nov 26, 2025
I would be 100% in favor of this! If anything I did rather make
.lock()panic on poison by default (with a different lock method returning a result that you can handle) than remove poison from the default mutex type. Without poisoning you are way too likely to keep limping around without making any forward progress ever again when an essential background thread panicked.mkeeter commentedon Nov 26, 2025
As a quick example, here's code which would panic with the current behavior, but would be UB (dereferencing a null pointer) with non-poisoning mutexes:
This is obviously a contrived example – and one could argue that updating to remove the
.unwrap()would force people to consider the edition changes – but in a large codebase, the path of least resistance would be for people to removethe
unwrap()and go along with their day, unaware that thestd::sync::Mutexbehavior has changed underneath them.(FWIW, I also like the idea of making
.lock()panic on poisoning, which could happen at an edition boundary!)hawkw commentedon Nov 26, 2025
It's worth noting that if
lock()is changed to always panic on poisoning at an edition boundary, it should probably only do so if!std::thread::panicking(); I've seen plenty of code wherePoisonErrors are always unwrapped except in aDropimpl, to avoid recursive panics causing the program to immediately abort.One downside of making the API unconditionally panic rather than return a
Resultis how it effects this case, actually. If poisoning is ignored when the thread is already panicking, then theDropimplementation can witness the potentially-violated invariants. I've seen code whereDropimplementations that do something not required for memory safety (i.e. updating timing data or counters or similar that are inside of a lock) just only do that action whenlock()returnsOk, and skip it if the lock has already been poisoned and theDropimpl was called in the midst of unwinding. Changing thelock()API to unconditionally panic instead does make that behavior unexpressable.bjorn3 commentedon Nov 26, 2025
We should still keep the non-panicking variant of the lock method around IMO. Just with a different name.
tgross35 commentedon Nov 27, 2025
This will need to be discussed by the team at some point, may as well nominate it now.
@rustbot label +I-libs-api-nominated
theemathas commentedon Nov 27, 2025
The situation is a bit more complicated. See #143612
oconnor663 commentedon Nov 28, 2025
I've also heard of (haven't personally seen) situations where poisoning does the opposite, putting the program into a permanent bad state when it would've been fine otherwise. Consider a service that spawns a thread for each connection and never joins them. If one request panics while it happens to hold an important lock, poisoning could cause ~all requests after that to panic. Then the service would be failing all requests until some watchdog happened to notice and reboot it. I think @mitsuhiko had a story about this somewhere.
oxalica commentedon Dec 4, 2025
There are some existing discussions about the future of
UnwindSafewhich is related to poisoning. IIUC, part of the reason why poisoning exists is to makeMutex: RefUnwindSafe, see docs ofUnwindSafe.XRef: rust-lang/libs-team#273 (cc @m-ou-se) and rust-lang/rfcs#2871 (cc @Diggsey)
Also I noticed that the current
RefCellimpl, which is akin to a single-threaded Mutex1, does not have poisoning, thus cannot "protect" user from forgetting unwinding exists during temporary invariant violation. I have not found any user who complains about that, which is definitely less than the number of users complaining about poison-by-default behavior of mutex.@mkeeter
It's not "would be" but "is already" a UB. It is mentioned in #143471 that the poison behavior is on best-effort basis and must NOT be relied for safety purpose. To prove it's unsound, we can practically make it segfault from a totally safe caller, by using a similar method given in that issue: playground.
Footnotes
RwLock, to be accurate. ↩21 remaining items
lyphyser commentedon Dec 8, 2025
Why? It seems me almost all Rust code already doesn't break unwind safety, and semver-breaking can be avoided by introducing new methods and deprecating older ones. catch_unwind is already sound if AssertUnwindSafe is not used, and scoped threads seem easily fixed with panic propagation from children threads to the parent thread at scope closure (which is a pretty intuitive thing to happen for a scoped thread).
Why do you think this is impossible and an effect system is needed?
purplesyringa commentedon Dec 8, 2025
This is somewhat off-topic for this thread, so I'll keep my response brief. The main issue is that the
UnwindSafetrait tracks whether a type is unwind-safe, but we're interested in is whether operations we perform are unwind-safe. This has multiple implications:You can have multiple mutating methods, where some objects are guaranteed not to panic (and should thus be usable in
catch_unwind/whatever), or perhaps handle panics gracefully, while others may break invariants on panic. You cannot currently do this on a per-function granularity, only on a per-type granularity, which would be akin to forcing all methods to consistently take either&mut selfor&self.UnwindSafeis derived for closures based on their captures, since closures are effectively juststructs. Yet closures can access non-unwind-safe objects not through captures: they can access thread-locals or receive mutable references through channels. Forcing all thread locals to beUnwindSafeis not an option, makingReceiver<T>: UnwindSaferequireT: UnwindSafewould be weird because the channel itself handles panics correctly, and it's entirely sound to have a thread that, say, reads fromReceiver<T>and writes toSender<T>in a loop. There's probably more real-world examples I'm missing.Type-granular unwind safety is not well-defined. Logically, primitives should not have any associated invariants: only user-constructed objects can. Yet while
i64implementsUnwindSafe, the primitive&mut Tdoesn't, and&Tonly implements it conditionally. There is no way to represent pure data.UnwindSafecan only track data invariants, not global state invariants: for example, you can't track stuff like locking the file system, since that doesn't access any data type.IMO,
UnwindSafeis fundamentally a category error. Tracking individual operations, i.e. introducing effects, would solve a lot of these problems. Of course, this is not a realistic approach, since it relies either on global inference, or on explicit annotations in all user code that introduces invariants, none of which is reasonable to do in a mature language.The fact that, as you say, "all Rust code doesn't break unwind safety" is, in my opinion, an implication of a particular combination of false positives and false negatives.
&mut Tbeing non-unwind-safe, means that all realistic Rust code does access non-unwind-safe objects, but the fact thatUnwindSafeis only checked for captures and not internally accessed objects means that this is seldom noticed.Poisoning mutexes really have the same issues. You can't say that panic during method A should poison the mutex, while panic during method B shouldn't. You can't use
Mutexif data is shared in some other way, say viaRefCellor atomics (though I can't imagine howPoisoncould possibly work with atomics). Adding non-poisoning mutexes arguably resolves the issue with non-well-defined unwind safety simply by making two types, though you still won't be able to storeMutexin pure data and let the consumer decide whether it can violate invariants (though mutexes are seldom stored in data, so this is not a practical worry). Poisoning can be opted into for global state invariants withMutex<()>, but it's not the default and you have to remember to do it. (Though mutex-only poisoning has another problem, which is that it conflates mutability and invariant violation, while you can clearly violate invariants in code using, say, atomics.)Zentheon commentedon Dec 16, 2025
I got curious when noticing that
nonpoison::Mutexwas a thing, and even more so when it required multiple feature flags to even use. It led me down these threads, so:Is very true.
There is a lot of stigma around using
unwrap(), and consequently I find myself mildly disgusted by using it in "fine if you know what you're doing" sorta places. Or then you could spend trice as long error handling what would have worked fine.At any rate, I don't see how the
nonpoisonAPI could be opposed much. If it's very common seelock().unwrap()without much afterthought, then clearly it's the more intuitive option most the time even besides the high-performance aspect. Especially when panics aren't even a concern in the first place. As a contrived example: Assigning statistics to a dedicated tracker at the end of a one-shotasync move(totally not what let me to findingnonpoisonin the first place). But anyway, I just found the discussion intriguing, not that I am versed in Rust internals.nicolas17 commentedon Feb 9, 2026
You seem to be implying
nonpoisonandlock().unwrap()are equivalent, when they're very much not. If a thread holding a mutex panics, non-poisoning mutexes will silently unlock, while poisoning mutexes will panic whenlock().unwrap()is called.nia-e commentedon Jul 22, 2026
I've been mulling over mutexes and our plans for 202x and I think there's simpler ways to proceed here which let us (mostly) have our cake and eat it too. It seems to me like a lot of the pain from 2024 mutexes does ultimately come from "
.lock().unwrap()is annoying", which i'm entirely sympathetic to. I also think that unwind safety in general is something that needs a lot more attention (cough allocator_api) and the mechanisms we provide right now are generally insufficient. However, mutex poisoning is probably one of the few ways in which we do actually steer code towards being resilient against unwind-unsafety. If even std is susceptible to unwind-unsafety bugs in paths that didn't unwind previously, I don't think I buy the argument that's been made before that it's all up to unsafe code to be resilient. Sure, it should be, but let's not cause extra unsoundness unnecessarily and instead make it easy to do the correct thing.We could keep poisoning default and just make the ergonomics nicer; edition-dependent name resolution is already going to be necessary for the current proposal, so why not do it for methods instead?
sync::poison::Mutexcan stay the default, with.lock()becoming equivalent to.lock().unwrap()and a separate.lock_checked()method. If we want to, we could expose the new semantics as.lock_forced()on <=2024 as well. This has the bonus advantage that swapping between poisoning and nonpoisoning mutexes becomes easier for implementors, since they'd expose the same API surface (except for panicking, ofc); just change theusestatement.(I realise this is not necessarily a new argument, but I would hope the points of evidence re: recent unsoundesses in std are relevant)
MatrixDev commentedon Jul 23, 2026
I don't really like hiding
unwraps. At least currently it is explicit and shows that something might go wrong. IMHO new developers (or anyone who is not aware of poisoning in general) will rarely read documentation about theMutex, it just simply such a basic primitive available in most languages.If Cloudflare incident has showed us anything - is that even
unwrapis not explicit enough for poisoning and can be ignored in relatively important infrastructure.bjorn3 commentedon Jul 23, 2026
The Cloudflare incident had nothing to do with poisoning at all. And I even disagree that the .unwrap() was really at fault there. Remember that the non-Rust version of the same code put everyone behind an infinite captcha, which is worse than a honest 500 error message as it wastes your time. The root cause was garbage data being fed into the server process that the server process wasn't able to handle correctly. If anything poisoning reduces the risk of such garbage data propagating out and into another thread that can handle the garbage even less.
FeldrinH commentedon Jul 23, 2026
That would endorse
.unwrap()as the correct default behavior for poisoning locks. While unwrapping is common I would argue that it is incorrect in most cases and only works out because the unhappy path where the lock is poisoned is extremely rare in practice.bjorn3 commentedon Jul 23, 2026
Panicking on poisoned mutexes is more correct than silently working on corrupted data (which is what
std::sync::nonpoisonwould do), but can be less correct than explicitly handling the poisoned data. So yes, panicking is a convenience tradeoff, but it is a better one that operating on corrupted data likestd::sync::nonpoisonwould do.nia-e commentedon Jul 23, 2026
These are reasonable points, but importantly, panicking is a safe and adjustable default. The vast majority of code cannot meaningfully make sense of possibly-broken values (as might be behind a poisoned mutex), and the poisoning of said mutex in and of itself is proof that unexpected panics have already started occuring in a given execution. There are of course going to be exceptions and cases where poison can be recovered from that might be overlooked, but unnecessary panic propagation is only a correctness issue; observing a value in an invalid state (especially without knowing it) can trigger a lot of UB
bjorn3 commentedon Oct 25, 2026
Another argument for keeping the poisoning mutexes the default is that the Google rust style guide explicitly forbids peeking into poisoned mutexes no matter the use case: https://google.github.io/styleguide/rustguide.html#handling-poison Using nonpoison mutexes by default would make it impossible to enforce that rule as you don't know if a mutex was held while another thread panicked (and thus semantically poisoned) unless a physical poison flag is set.
Manishearth commentedon Oct 29, 2026
FWIW I would not recommend using that as justification for this discussion. The style rule was added in a very early phase of designing the style rules and stuck around. It matches a general "don't do sketchy things" policy that is behind those style rules. We could reevaluate it if Rust removed poisoning.