This is an archive of the discontinued LLVM Phabricator instance.

[RFC] Thread safety analysis: Track status of scoped capability
Changes PlannedPublic

Authored by aaronpuchert on Aug 23 2018, 3:04 PM.

Details

Summary

We now warn when acquiring or releasing a scoped capability a second
time, not just if the underlying mutexes have been acquired or released
a second time.

Whether we unlock the underlying mutexes now depends on the status of
the scoped capability. So we don't unlock them if we are already
released, and we unlock them (with warning even on destruction) if we
are currently locked.

Diff Detail

Event Timeline

aaronpuchert created this revision.Aug 23 2018, 3:04 PM

I'm not certain how I feel about this as it seems to be removing functionality users may be relying on that I think is still technically correct. Can you describe more about why you think this should change?

Sure. As I wrote in the commit message, I'm not sure about it myself, but I think it's worth a discussion. Maybe I should have tagged it as RFC.

Releasable scopes need a way of knowing whether the lock is currently held to prevent double unlocking in the destructor. There are basically two ways to achieve that: one can ask the underlying mutex if it is locked, or keep track of the lock status. The second might be the only option available, as not all mutex implementations offer a way to find out if they are currently held.

Suppose we have something like:

class ReleasableScope {
  Mutex *mu;
  bool Locked;

public:
  ReleasableScope(Mutex *mu) : mu(mu), Locked(true) {
    mu->Lock();
  }
  void Unlock() { mu->Unlock(); Locked = false; }
  ~ReleasableScope() { if (Locked) mu->Unlock(); }
};

Then it can be a problem if someone works directly on the underlying mutex without informing the scoped capability:

Mutex mu;

void test1() {
  ReleasableScope scope(&mu);
  mu.Unlock();
} // ~ReleasableScope unlocks mu again, but we don't warn about double unlock.

void test2() {
  ReleasableScope scope(&mu);
  scope.Unlock()
  mu.Lock();
} // ~ReleasableScope does not unlock, but we don't warn about the lock still being held.

In the LockableFactEntry we call mu the "managed" mutex of scope and I think that implies that for its lifetime only scope should touch mu. What we do in test1 is akin to manually deleteing the underlying pointer of a std::unique_ptr. I would actually consider releasing mu again in ScopedLockableFactEntry::handleUnlock in the epilogue of test1 and not releasing it in the epilogue of test2, because we think it is still locked/already unlocked. Then we would emit warnings in both cases.

On the other hand, if the scoped capability can (and does) ask the underlying mutex for its state before doing anything, we might be completely fine, as you stated.

So I'm really not sure, but I think there are some arguments that can be made for introducing this warning (and possibly more).

We could also consider making this part of the -Wthread-safety-beta warnings, so that we don't break anyone's -Werror=thread-safety builds.

Use Locked flag to determine whether to unlock on destruction

Instead of unlocking the mutexes that are still available while not complaining about those that aren't, we use the status of the scoped capability to determine whether to unlock the underlying mutexes.

This way we will attempt to unlock the mutex even if the user unlocked it manually, warning if it is no longer available, and we will not attempt to unlock if we are already released, hence warning if it is manually acquired again.

aaronpuchert retitled this revision from Thread safety analysis: Warn on double (un-)lock of scoped capability to Thread safety analysis: Track status of scoped capability.Aug 25 2018, 12:53 PM
aaronpuchert edited the summary of this revision. (Show Details)
aaronpuchert retitled this revision from Thread safety analysis: Track status of scoped capability to [RFC] Thread safety analysis: Track status of scoped capability.Aug 25 2018, 12:57 PM
aaronpuchert planned changes to this revision.Sep 13 2018, 4:03 PM

This doesn't work with loops yet:

void relockLoop() {
  RelockableExclusiveMutexLock scope(&mu);

  while (b) {
    scope.Unlock(); // releasing mutex 'scope' that was not held
    scope.Lock();   // acquiring mutex 'mu' that is already held
  }
}

There should be no warnings here — this code is fine.

delesley added inline comments.Sep 19 2018, 4:18 PM
lib/Analysis/ThreadSafety.cpp
929

It's been a while since I last looked at this code, but I don't think you can use mutable fields in a FactEntry. The analysis creates a FactSet for each program point, but each FactSet simply has pointers (FactIDs) for the underlying FactEntries. If you change the definition of a FactEntry, it will change that definition for every program point. So if you do an unlock on one side of a branch, and change the underlying FactEntry to reflect that, then analysis will then think you have also done the unlock on the other side of the branch.

Any changes should always be done by adding or removing entries from the FactSet, not by mutating the underlying FactEntries.

aaronpuchert added a comment.EditedSep 20 2018, 5:45 PM

Thanks for pointing out my error! Ignoring the implementation for a moment, do you think this is a good idea or will it produce too many false positives? Releasable/relockable scoped capabilities that I have seen keep track of the status, so it makes sense, but maybe there are others out there.

lib/Analysis/ThreadSafety.cpp
929

That explains why there are problems with back edges. Now that you say it I'm actually aware of how the FactManager and FactSet work, but apparently I didn't consider that here.

Would you mind if I make a separate change that returns only const FactEntry* from the FactManager? Maybe that makes it harder to write wrong code in the future.

Thanks for pointing out my error! Ignoring the implementation for a moment, do you think this is a good idea or will it produce too many false positives? Releasable/relockable scoped capabilities that I have seen keep track of the status, so it makes sense, but maybe there are others out there.

I think this is probably a good idea. Code which manipulates the underlying mutex while it is being managed by another object is a recipe for trouble. It's hard for me to guess how many false positives it will generate without actually running it over our code base, which I personally don't have time to do right now. It should definitely go in -Wthread-safety-beta so it won't break the build. Unfortunately, the before/after checks are still in thread-safety-beta, and they should really be moved out of beta before something else is moved in. The good news is that Matthew O'Niel just volunteered to do that. That is, unfortunately, not a trivial operation, so it may be some weeks before he's done.

Any changes should always be done by adding or removing entries from the FactSet, not by mutating the underlying FactEntries.

To make that clearer in the code, I made FactEntrys immutable that are managed by FactManager in rC342787.

It should definitely go in -Wthread-safety-beta so it won't break the build. Unfortunately, the before/after checks are still in thread-safety-beta, and they should really be moved out of beta before something else is moved in. The good news is that Matthew O'Niel just volunteered to do that. That is, unfortunately, not a trivial operation, so it may be some weeks before he's done.

That's Ok for me. It's not something that I terribly need, but it seemed to make things more consistent.

Does the migration of the before/after checks include changes to how it is handled? Because I wondered why it doesn't work on S-expressions like the rest of the analysis, but just ValueDecls. That leads to some false positives with more complex expressions:

class A {
public:
  Mutex mu1;
  Mutex mu2 ACQUIRED_AFTER(mu1);
};

class B {
  A a1, a2;
  Mutex lm ACQUIRED_AFTER(a1.mu2);

public:
  void f() {
    a2.mu2.Lock();
    a1.mu1.Lock();    // warns "mutex 'mu1' must be acquired before 'mu2'", but they are on different objects
    a1.mu1.Unlock();
    a2.mu2.Unlock();
  }

  void g() {
    lm.Lock();
    a1.mu1.Lock();
    a1.mu1.Unlock();
    a2.mu1.Lock();    // Warns "mutex 'mu1' must be acquired before 'lm'", but lm talks only about a1.mu2.
    a2.mu1.Unlock();
    lm.Unlock();
  }
};

But maybe that's not the right place to discuss this.