Skip to content

Edition 2027: Consider replacing std::sync::Mutex and other locks with std::sync::nonpoison versions #149359

Open

Description

@tgross35

View all comments

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).

Related:

Activity

  1. added
    T-libs-api[DEPRECATED; DO NOT USE]
    maybe-future-editionSomething we may consider for a future edition.
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    on Nov 26, 2025
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Nov 26, 2025
  3. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Nov 26, 2025
  4. anp commented on Nov 26, 2025

    @anp
    Contributor

    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.

  5. bjorn3 commented on Nov 26, 2025

    @bjorn3
    Member

    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.

  6. mkeeter commented on Nov 26, 2025

    @mkeeter
    Contributor

    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 valid
    struct PointerWrapper(Mutex<*mut u32>);
    
    const SHOULD_PANIC: bool = false;
    
    fn poke_pointer(p: &PointerWrapper) {
        let mut p = p.0.lock().unwrap();
        // Briefly break invariants to manipulate the pointer
        let prev = std::mem::replace(&mut *p, std::ptr::null_mut());
        let next = do_something_that_could_panic(prev);
        *p = next;
    }
    
    fn do_something_that_could_panic(ptr: *mut u32) -> *mut u32 {
        if SHOULD_PANIC {
            panic!("oh no");
        }
        ptr
    }
    
    fn main() {
        let mut 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!)

  7. hawkw commented on Nov 26, 2025

    @hawkw
    Contributor

    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.

  8. bjorn3 commented on Nov 26, 2025

    @bjorn3
    Member

    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.

  9. tgross35 commented on Nov 27, 2025

    @tgross35
    MemberAuthor

    This will need to be discussed by the team at some point, may as well nominate it now.

    @rustbot label +I-libs-api-nominated

  10. theemathas commented on Nov 27, 2025

    @theemathas
    Contributor

    it should probably only do so if !std::thread::panicking();

    The situation is a bit more complicated. See #143612

  11. oconnor663 commented on Nov 28, 2025

    @oconnor663
    Contributor

    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.

  12. oxalica commented on Dec 4, 2025

    @oxalica
    Contributor

    There are some existing discussions about the future of UnwindSafe which is related to poisoning. IIUC, part of the reason why poisoning exists is to make Mutex: RefUnwindSafe, see docs of UnwindSafe.
    XRef: rust-lang/libs-team#273 (cc @m-ou-se) and rust-lang/rfcs#2871 (cc @Diggsey)

    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.

    @mkeeter

    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.

    Footnotes

    1. RwLock, to be accurate. ↩

  13. 21 remaining items

  14. lyphyser commented on Dec 8, 2025

    @lyphyser

    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?

  15. purplesyringa commented on Dec 8, 2025

    @purplesyringa
    Contributor

    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:

    1. 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.

    2. 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.

    3. 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.

    4. 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.)

  16. Zentheon commented on Dec 16, 2025

    @Zentheon

    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.

  17. nicolas17 commented on Feb 9, 2026

    @nicolas17

    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.

  18. nia-e commented on Jul 22, 2026

    @nia-e
    Member

    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)

  19. MatrixDev commented on Jul 23, 2026

    @MatrixDev

    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.

  20. bjorn3 commented on Jul 23, 2026

    @bjorn3
    Member

    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.

  21. FeldrinH commented on Jul 23, 2026

    @FeldrinH

    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.

  22. bjorn3 commented on Jul 23, 2026

    @bjorn3
    Member

    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.

  23. nia-e commented on Jul 23, 2026

    @nia-e
    Member

    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

  24. added
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    and removed
    T-libs-api[DEPRECATED; DO NOT USE]
    on Aug 12, 2026
  25. bjorn3 commented on Oct 25, 2026

    @bjorn3
    Member

    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.

  26. Manishearth commented on Oct 29, 2026

    @Manishearth
    Member

    27. 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

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-discussionCategory: Discussion or questions that doesn't represent real issues.T-libsRelevant to the library team, which will review and decide on the PR/issue.maybe-future-editionSomething we may consider for a future edition.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions