Summary
@orthur2 @jeffoodchain Could you help verify the soundness issue below and discuss the appropriate API contract and solution?
MappedRwLockWriteGuard::downgrade and OwnedMappedRwLockWriteGuard::downgrade retain a pointer obtained from an arbitrary mutable projection while allowing other readers of the original value. A safe-code reproducer triggers undefined behavior under Miri. This issue tracks verification and the solution; the attempted fix in #358 has been closed pending discussion.
Reproducer
The following entirely safe program reproduces the problem on commit 44fc2d8:
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 later mutable access through outer.lock() invalidates the shared reference. The corresponding owned mapped guard reproducer also fails.
Soundness concern
The mutable projection &mut T -> &mut U can reach interior-mutable storage using exclusive access, as Mutex::get_mut does here. After downgrading, another reader of the original T can mutate that storage through its interior-mutability API while the mapped guard still exposes &U.
Both Mutex<u8> and u8 are Send + Sync, so adding those bounds alone does not address this example. The expected behavior is that safe guard operations cannot permit mutation that invalidates a live shared reference.
History and possible solutions
Keeping all exclusive permits appears sound but continues blocking readers, changing the downgrade contract. Downgrading the original write guard before applying a shared projection is another option: RwLockReadGuard::map(write.downgrade(), |root| &root.field) retains the lock throughout. A convenience API accepting a projection from the original &T could express the same boundary, but cannot simply reuse an arbitrary mutable projection.
Could you confirm whether the reproducer and reasoning establish that these APIs are unsound, and help decide whether to remove mapped downgrading or support it under a different contract?
Summary
@orthur2 @jeffoodchain Could you help verify the soundness issue below and discuss the appropriate API contract and solution?
MappedRwLockWriteGuard::downgradeandOwnedMappedRwLockWriteGuard::downgraderetain a pointer obtained from an arbitrary mutable projection while allowing other readers of the original value. A safe-code reproducer triggers undefined behavior under Miri. This issue tracks verification and the solution; the attempted fix in #358 has been closed pending discussion.Reproducer
The following entirely safe program reproduces the problem on commit
44fc2d8:Miri reports that the later mutable access through
outer.lock()invalidates the shared reference. The corresponding owned mapped guard reproducer also fails.Soundness concern
The mutable projection
&mut T -> &mut Ucan reach interior-mutable storage using exclusive access, asMutex::get_mutdoes here. After downgrading, another reader of the originalTcan mutate that storage through its interior-mutability API while the mapped guard still exposes&U.Both
Mutex<u8>andu8areSend + Sync, so adding those bounds alone does not address this example. The expected behavior is that safe guard operations cannot permit mutation that invalidates a live shared reference.History and possible solutions
Keeping all exclusive permits appears sound but continues blocking readers, changing the downgrade contract. Downgrading the original write guard before applying a shared projection is another option:
RwLockReadGuard::map(write.downgrade(), |root| &root.field)retains the lock throughout. A convenience API accepting a projection from the original&Tcould express the same boundary, but cannot simply reuse an arbitrary mutable projection.Could you confirm whether the reproducer and reasoning establish that these APIs are unsound, and help decide whether to remove mapped downgrading or support it under a different contract?