Live data from Hacker News

Rust's Sneaky Deadlock With `if let` Blocks

brooksblog.bearblog.dev

61–70 of 85 posts

Re: Rust's Sneaky Deadlock With `if let` Blocks

#61
post #48

Earlier quoted context omitted.

That's arguably true, but only insofar as it makes sense to talk about a mutex "owning" "data". There are many kinds of data that can't be owned by the language runtime like this (think about async/shared/DMA buffers, register blocks on foreign hardware, memory-mapped database files), but that still clearly need synchronization. Even things like coarse-grained locks taken at the subsystem level (e.g. to avoid having…

That is true, but I don't believe Rust imposes a cost here. The worst case degrades to a `Mutex ` and then whatever's being guarded being managed separately.

From an API design point of view, I would always encapsulate the logically mutable state in some type `Foo`, even if it doesn't literally own the memory, and then expose a safe API using `Mutex`.

Idiomatically, you would then make `Foo::new_unchecked()` unsafe, with the precondition that nobody else is accessing the same external resource, and that `Foo::new_unchecked()` is only ever called once.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#62
post #33

A lock taken in the conditional test term should lock for the duration of the if-clause. If you release the lock early, you add the risk of a race condition, where some other thread executes between the test and the execution of the if or else clause, changing the state.

Right. But the problem is that with an 'if let', it also locks for the 'else', which is not intuitive or expected.

The problem is that the syntax of 'if let' makes the programmer assume a particular scoping, but its desugaring implies something else.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#63

Earlier quoted context omitted.

Yes, allowing this to execute would be very unsound: let lock = RwLock::new(Box::new(111)); let r: &i32 = &**lock.read().unwrap(); // points to 111 *lock.write().unwrap() = Box::new(222); // allocates a new Box and deallocates 111 println!("{}", *r); // use after free

It can be done safely with an upgrade method that requires an owned read guard. The RwLock implementation provided by the parking_lot crate supports this safely: let lock = RwLock::new(Box::new(111)); let read = lock.upgradable_read(); let r: &i32 = &**read; // points to 111 *RwLockUpgradableReadGuard::upgrade(read) = Box::new(222); // error[E0505]: cannot move out of `read` because it is borrowed println!("{}", *r);

The problem is that in the 'if let' case, the 'else' block has no access to the read guard. It's out of scope, except that the compiler hasn't dropped it yet.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#64
post #57
post #52

Earlier quoted context omitted.

It’s getting fixed soon - https://github.com/rust-lang/rust/issues/124085

If the change is targeting a rust edition it's being treated as a feature/improvement not as a compiler bug. If there isn't a spec that explicitly says that this is expected behavior (and as far as I know there isn't a concrete spec for the rust language) it should be instead treated as a backwards compatible bug fix

No, it's being treated as a breaking change, which it is. It is not a backwards-compatible bug fix.

You are correct that there is no concrete spec for the Rust language; the current state of the compiler and stdlib is the "spec". So this is a breaking change to the "spec", and requires a new edition.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#65

Clippy already has an error for this pattern with Mutex. It should be trivial to extend it to cover RwLock. error: calling `Mutex::lock` inside the scope of another `Mutex::lock` causes a deadlock --> src/main.rs:5:5 | 5 | if let Some(num) = *map.lock().unwrap() { | ^ --- this Mutex will remain locked for the entire `if let`-block... | _____| | | 6 | | eprintln!("There's a number in there: {num}"); 7 | | } else { 8 |…

There are a lot of alternative lock implementations that are used all the time, with the most common ones probably being tokios RwLock/Mutex and parking_lot. That lint won't help for those.

There was an idea floated for another lint `#[diagnostics::lint_as_temporary(reason = "...")]`, which is supposed to be added by implementors of such locks. https://github.com/rust-lang/rust/issues/131154#issuecomment...

Re: Rust's Sneaky Deadlock With `if let` Blocks

#66
post #2

This is why pthread_mutex_lock() and pthread_mutex_unlock() will always be kings - it is clear when things are locked and when they are unlocked. Nobody needs to write articles warning you about pthread_mutex being taken randomly by syntactic sugar and not released till a magic later time due to the same sugar. Ditto for RWlock/XYZloc/ABClock/etc...

No thanks; it's so so so easy to forget to unlock a mutex; I'd rather the language and stdlib just make it impossible for me to do so.

Ultimately this isn't really a problem with how Rust's Mutex/RwLock/etc. works, it's just a poor choice with how the lifetime of the lock guard is figured in the 'if let' case. This poor choice will be fixed in the 2024 Rust edition, and this problem will go away.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#67

Earlier quoted context omitted.

There are however many PRs about people ignoring the return values of pthread_mutex_lock(), or missing an unlock on one route through a function.

and nothing prevents you from explicitly dropping lock guards in rust either actually it's a common pattern in complicated multi threaded code or similar to require that

So what? If you drop the lock guard, you get a straightforward compiler error if you later try to access the data in it. You don't get a runtime bug that you have to track down like you get if you forget a pthread_mutex_unlock().

Re: Rust's Sneaky Deadlock With `if let` Blocks

#68

Earlier quoted context omitted.

This really reads like satire but feels like the author is actually being sincere. I’ve seen so many deadlocks happening due to pthread mutexes. Certainly way more than this syntactic corner case. It’s so common in fact that I saw a codebase that made their mutexes recursive to avoid having to think about it.

[flagged]

Deadlocks are a sign of normal humans getting tripped up by crappy abstractions.

If you're going to claim you've never written a deadlock in your life, well... I'm not sure I'd believe that.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#69

While I never dove deep into Rust, until now I have kinda naively expected for some reason that Rust's lifetimes and ownership model prevents these trivial deadlocks at compile time. Thinking about it now, there's no reason why it would. Still lots of footguns. Also, from my experience, acquiring and releasing a mutex multiple times within a single code path feels to me like a smelly, subtly faulty code. Are there le…

> I have kinda naively expected for some reason that Rust's lifetimes and ownership model prevents these trivial deadlocks at compile time.

A `Mutex`/`RwLock` is doing borrow checking at runtime, effectively replacing the compile time one.

Re: Rust's Sneaky Deadlock With `if let` Blocks

#70
One way to solve this at the language level would be to require that each lock has a specified priority associated with it, and have the compiler enforce a constraint that locks can only be obtained in the order of their priority.

For each statement, the compiler could keep track of the current "lock priority". If an attempt is made to obtain a new lock with a lower or equal priority, the compiler would reject this. Otherwise, the compiler would use the priority of the new lock as the lock priority for all statements within the scope of the block in which that lock is held.

Aside from modifying the language, the same basic idea could be implemented at the library level by exposing a lock type which still requires a priority to be specified at creation time, but tracks the current lock priority (per thread) at runtime and panics when the constraint is violated. Since the panics would be deterministic, they and the book-keeping required to track the current lock priority could be enabled only in debug mode.

Post reply on HN