Skip to content

Investigate unsound mapped RwLock write guard downgrading #362

Description

@tisonkun

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?

Activity

  1. Soumya2004-java commented on Oct 11, 2026

    @Soumya2004-java

    Hi @tisonkun and maintainers,

    I'd like to work on this issue if it's still available.

    I'll start by reproducing the problem under Miri and tracing how the mapped write guards handle mutable projections during downgrading. I also want to look closely at the proposed alternatives, especially downgrading the original guard before applying a shared projection, to understand which approach preserves soundness without unnecessarily breaking the API.

    I'll share my findings before making any API changes and add regression tests for whichever solution we agree on.

    If no one is currently working on this, could you please assign the issue to me? I'd be happy to take it from investigation through implementation and testing.

    Thanks!

  2. tisonkun commented on Oct 11, 2026

    @tisonkun
    MemberAuthor

    @Soumya2004-java Feel free to send a PR. But the more important things are reasoning whether the issue exists badly and whether we should fix it or remove the method, for what reason. Please do not just put some random AI generated PR and push the community to review that unreviewed content.

  3. orthur2 commented on Oct 11, 2026

    @orthur2
    Contributor

    Hi @Soumya2004-java, sorry for the overlap! I had already confirmed this issue in #358 and started working on a fix #365 a few hours ago. I just opened a PR before seeing your comment.

    You’re still very welcome to review the PR or share your thoughts on the proposed solution.

    If you’re interested in contributing elsewhere, #169 (upgradable RwLock) and #324 (object pool recycling diagnostics) could be good starting points. You can also explore other issues labeled good first issue or help wanted.

    Thanks for your interest in contributing!

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions