Skip to content

[DRAFT] [starvation] model std::scoped_lock with several mutexes and std::adopt_lock - #2172

Draft
VladimirMakaev wants to merge 2 commits into
facebook:mainfrom
VladimirMakaev:starvation-scoped-lock
Draft

VladimirMakaev wants to merge 2 commits into
facebook:mainfrom
VladimirMakaev:starvation-scoped-lock

Conversation

@VladimirMakaev

@VladimirMakaev VladimirMakaev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #2182 → #2172 (review only the top commit; lands after #2182)

Infer parsed std::scoped_lock like the other guards, as (mutex[, tag]), and a guard could own
only one lock. So scoped_lock l(m1, m2) acquired nothing, three or more mutexes were not parsed,
and the std::adopt_lock forms never released their mutexes. Starvation missed deadlocks and
RacerD missed races:

struct C {
  std::mutex m1, m2, m3;
  void thread1() {
    std::scoped_lock l(m1, m2);
    std::lock_guard<std::mutex> g(m3);
  }
  void thread2() { // Infer missed: deadlock with thread1
    std::lock_guard<std::mutex> g(m3);
    std::scoped_lock l(m1, m2);
  }
};

GuardConstruct now carries a list of locks, and std::scoped_lock gets its own parser, which
recognises the std::adopt_lock_t tag by type; destroying a guard releases all its locks. A
multi-mutex acquisition (std::lock, std::scoped_lock) that relocks a held non-recursive mutex
is now a self deadlock even when another of its mutexes is recursive.

Test plan

New cpp/starvation/scoped_lock.cpp, with an FN_ test for std::try_lock followed by
std::adopt_lock, and new cases in cpp/racerd/scoped_lock.cpp. cpp/starvation now compiles
with -std=c++17; its only other effect is that guaranteed copy elision makes the copy-initialised
guard in custom_guards.cpp recognised, so that test loses its FN_ prefix. The codetoanalyze
tests pass.

@meta-cla meta-cla Bot added the CLA Signed label Oct 2, 2026
@VladimirMakaev
VladimirMakaev force-pushed the starvation-scoped-lock branch from ef80b39 to a5d8732 Compare October 4, 2026 02:42
Starvation only knows a fixed list of scoped guards. With any other (eg android::Mutex::Autolock),
the lock taken by the guard's constructor was read as a caller lock, so all guards collapsed into
one lock: inversions were missed and self deadlocks reported.

```cpp
#include <mutex>

struct Guard {
  explicit Guard(std::mutex& m) : m_(m) { m_.lock(); }
  ~Guard() { m_.unlock(); }
  std::mutex& m_;
};

class C {
  std::mutex mu1_, mu2_;
 public:
  void thread1() { Guard a(mu1_); Guard b(mu2_); } // false positive: self deadlock
  void thread2() { Guard b(mu2_); Guard a(mu1_); } // same; Infer missed: deadlock with thread1
};
```

Callee locks are now substituted for every argument and call depth. A local object whose
constructor (or by-value return, if its destructor releases a lock) leaves one lock held becomes a
guard of that lock. A lock a callee released before an event is not held by its callers there (eg
`mu.unlock(); sleep(); mu.lock();`). android::Mutex and Autolock are modelled, tryLock and
timedLock as trylocks returning zero on success; RacerD now reports races in classes using them.

Limitations: callee locks and unlocks the caller cannot express are dropped, missing deadlocks
through objects returned by calls (eg Java singletons) and keeping locks released through a local
guard struct.

## Test plan

New tests in c/starvation/lock_wrappers.c, cpp/starvation (android_mutex.cpp, custom_guards.cpp,
release_in_callee.cpp, substitution.cpp), cpp/racerd/android_mutex.cpp and
java/starvation/Parameters.java, with FP_/FN_ tests for the limitations. The codetoanalyze tests
pass.
…pt_lock

Infer parsed `std::scoped_lock` like the other guards, as `(mutex[, tag])`, and a guard could own
only one lock. So `scoped_lock l(m1, m2)` acquired nothing, three or more mutexes were not parsed,
and the `std::adopt_lock` forms never released their mutexes. Starvation missed deadlocks and
RacerD missed races:

```cpp
struct C {
  std::mutex m1, m2, m3;
  void thread1() {
    std::scoped_lock l(m1, m2);
    std::lock_guard<std::mutex> g(m3);
  }
  void thread2() { // Infer missed: deadlock with thread1
    std::lock_guard<std::mutex> g(m3);
    std::scoped_lock l(m1, m2);
  }
};
```

`GuardConstruct` now carries a list of locks, and `std::scoped_lock` gets its own parser, which
recognises the `std::adopt_lock_t` tag by type; destroying a guard releases all its locks. A
multi-mutex acquisition (`std::lock`, `std::scoped_lock`) that relocks a held non-recursive mutex
is now a self deadlock even when another of its mutexes is recursive.

## Test plan

New `cpp/starvation/scoped_lock.cpp`, with an `FN_` test for `std::try_lock` followed by
`std::adopt_lock`, and new cases in `cpp/racerd/scoped_lock.cpp`. `cpp/starvation` now compiles
with `-std=c++17`; its only other effect is that guaranteed copy elision makes the copy-initialised
guard in `custom_guards.cpp` recognised, so that test loses its `FN_` prefix. The codetoanalyze
tests pass.
@VladimirMakaev
VladimirMakaev force-pushed the starvation-scoped-lock branch from a5d8732 to 0360092 Compare October 5, 2026 02:41

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant