Repository navigation
Conversation
|
@orthur2 @jeffoodchain Could you help verify the mapped RwLock write guard soundness issue described here and discuss the right solution? I've closed this PR to leave the API decision open for discussion. The following entirely safe program triggers UB under Miri on the base commit ( use asyncband::rwlock::{RwLock, RwLockWriteGuard};
use std::sync::Mutex;
fn main() {
let lock = RwLock::new(Mutex::new(0u8));
let write = RwLockWriteGuard::map(lock.try_write().unwrap(), |inner| {
inner.get_mut().unwrap()
});
let read = write.downgrade();
let shared = &*read;
let outer = lock.try_read().unwrap();
*outer.lock().unwrap() = 1;
println!("{shared}");
}Miri reports that the mutable access through The mapped downgrade methods were introduced in #65 and first released in mea 0.4.2; #62 and the follow-up discussion in #61 provide the original feature context. Could you check whether the reproducer and this reasoning establish an unsound API, and which contract we should support? Keeping exclusive permits would block other readers; downgrading the original write guard before a shared projection, or providing an API that projects from the original |
|
Thanks for catching this. I can reproduce it with Miri, and the owned version fails the same way. Bounds won't help, since putting the mutex behind a The mistake in #65 was treating downgrade as a pure permit change. A I'd remove both methods as you proposed. tokio and lock_api don't offer downgrade on mapped write guards either. Keeping the write permits would make the method misleading, and an |
Summary
Remove
downgradefrom borrowed and owned mapped write guards: retaining an arbitrary mutable projection while allowing readers of the original value is unsound. Both APIs were introduced by #65 and first shipped in mea v0.4.2 (2025-08-30); they remain present in v0.7.3. Document the breaking migration to downgrading the original write guard before using read-guardmaporfilter_map.Add compile-fail regressions for both safe-code reproducers, exercise shared projections with interior mutability under Miri, and update existing projection tests and examples.
Fix the independent oneshot reclamation bug exposed by CI in a separate commit. Its sender finalization helper now accepts the channel pointer instead of retaining an
&selfparameter while the receiver can free the allocation.Design Notes
This entirely safe program triggers undefined behavior under default Miri on the base commit:
The mutable projection bypasses the inner mutex using exclusive access. Downgrading then lets another reader lock that mutex and mutate the
u8whilesharedremains live. Miri reports that the later unique retag invalidates its shared read-only tag. The owned variant fails identically. BothMutex<u8>andu8areSend + Sync, so adding trait bounds does not fix this. A separateCell<u8>reproducer also detects a data race after sending a mapped write guard to another thread and downgrading there.Keeping all write permits would be safe, but would keep blocking readers and change the downgrade contract. A replacement projection from the original
&Tcan be sound; the existingRwLockReadGuard::map(write.downgrade(), |root| &root.field)composition already provides this without an unlocked interval, with an owned equivalent. A convenience API acceptingFnOnce(&T) -> &Ucould express the same safety boundary, but cannot reuse an arbitrary mutable projection. Tokio likewise prohibits mapped write guard downgrading and its write-guarddowngrade_maprequires a shared projection. Projections that require exclusive access must keep their mapped write guard.The oneshot failure was reproduced with the CI toolchain (
nightly-2026-10-11), Linux target, strict provenance, symbolic alignment checks, and seeds0..4. In seed 1,concurrent_drop_sender_and_poll_to_completionreclaimed the allocation whilefinish_sender_awakening(&self, ...)still held a protected argument borrow. PassingNonNull<Channel<T>>allows that borrow to end at terminal publication; the helper never accesses the channel afterward. Existing integration and state-machine tests cover both normal publication and receiver cancellation.Validation:
cargo x test(598 passed, including both compile-fail regressions),cargo x check, andcargo x lint. Oneshot integration tests (19 per seed) and internal state tests (2 per seed) pass under the matching CI Miri toolchain, flags, Linux target, and all four seeds. The borrowed, owned, and cross-thread mapped-downgrade reproducer programs now fail to compile with E0599.