This is an archive of the discontinued LLVM Phabricator instance.

Thread safety analysis: Allow lock upgrading and downgrading
ClosedPublic

Authored by aaronpuchert on Jul 15 2018, 2:34 PM.

Details

Summary

We can now have methods that release a locked in shared mode and acquire
it in exclusive mode or the other way around. The fix was just to
release the locks before acquiring them.

Also added a few test cases for non-generic unlock methods and removed
an unnecessary const_cast.

Diff Detail

Event Timeline

aaronpuchert created this revision.Jul 15 2018, 2:34 PM

Ping? The functional changes should be minimal.

test/SemaCXX/warn-thread-safety-analysis.cpp
38–39

This was necessary to make sure we produce warnings of the kind releasing mutex '...' using exclusive access, expected shared access for both configurations.

I'm going to let @delesley give the final sign off on this as he is more familiar with this part of the analysis, but I think this looks reasonable.

delesley accepted this revision.Jul 25 2018, 2:38 PM

Looks good to me. Thanks for the patch, and my apologies for the slow response. (I'm on a different project now, so I'm afraid thread safety doesn't always get the attention from me that it deserves.)

-DeLesley
This revision is now accepted and ready to land.Jul 25 2018, 2:38 PM

No problem. Thanks for the review! Would be nice if you or @aaron.ballman could commit this, as I don't have commit access.

aaron.ballman closed this revision.Jul 26 2018, 6:03 AM

I've commit in r338024, thank you for the patch!

phosek added a subscriber: phosek.Aug 3 2018, 8:00 PM

This change broke the acquire+release case. Concretely, in Flutter we have this code: https://github.com/flutter/engine/blob/master/lib/ui/isolate_name_server/isolate_name_server.h#L26. This worked fine previously, but after this change we're getting an error in https://github.com/flutter/engine/blob/master/lib/ui/isolate_name_server/isolate_name_server_natives.cc#L19 and many other places like this one:

../../third_party/flutter/lib/ui/isolate_name_server/isolate_name_server_natives.cc:19:33: error: releasing mutex 'name_server->mutex_' that was not held [-Werror,-Wthread-safety-analysis]
  Dart_Port port = name_server->LookupIsolatePortByName(name);
                                ^
../../third_party/flutter/lib/ui/isolate_name_server/isolate_name_server_natives.cc:24:1: error: mutex 'name_server->mutex_' is still held at the end of function [-Werror,-Wthread-safety-analysis]
}
^
../../third_party/flutter/lib/ui/isolate_name_server/isolate_name_server_natives.cc:19:33: note: mutex acquired here
  Dart_Port port = name_server->LookupIsolatePortByName(name);

Would it be possible revert this change? The old logic was "all acquires; then all releases" and the new logic is "all releases; then all acquires" but I think this needs fancier logic with actual bookkeeping to detect the upgrade case without breaking the acquire+release case.

Could you explain what annotations like LOCK_UNLOCK are useful for? What do they check? The annotation should certainly not be necessary.

Shouldn't you just use REQUIRES(!...) or EXCLUDES(...)? If a function locks and unlocks a mutex, I don't see a reason to have annotations on it, other than for preventing double locks. But for that we have negative capabilities.

phosek added a comment.Aug 4 2018, 3:21 PM

Could you explain what annotations like LOCK_UNLOCK are useful for? What do they check? The annotation should certainly not be necessary.

Shouldn't you just use REQUIRES(!...) or EXCLUDES(...)? If a function locks and unlocks a mutex, I don't see a reason to have annotations on it, other than for preventing double locks. But for that we have negative capabilities.

The purpose here indeed is to avoid double locks. I tried using EXCLUDES(...) but that doesn't work because [RegisterIsolatePortWithName](https://github.com/flutter/engine/blob/master/lib/ui/isolate_name_server/isolate_name_server.h#L31) calls [LookupIsolatePortByNameUnprotected](https://github.com/flutter/engine/blob/master/lib/ui/isolate_name_server/isolate_name_server.h#L39) which has EXCLUSIVE_LOCKS_REQUIRED(...) annotation. I also tried using the negative annotation but that reports far too many warnings in the existing code which makes it unusable.

I'm fine changing the code, but unless there's a simple workaround I'd still argue for a revert, because the change even if correct has broken an existing usage pattern that worked fine for a long time before and is used in large codebases.

Could you explain what annotations like LOCK_UNLOCK are useful for? What do they check? The annotation should certainly not be necessary.

Shouldn't you just use REQUIRES(!...) or EXCLUDES(...)? If a function locks and unlocks a mutex, I don't see a reason to have annotations on it, other than for preventing double locks. But for that we have negative capabilities.

The purpose here indeed is to avoid double locks. I tried using EXCLUDES(...) but that doesn't work because [RegisterIsolatePortWithName](https://github.com/flutter/engine/blob/master/lib/ui/isolate_name_server/isolate_name_server.h#L31) calls [LookupIsolatePortByNameUnprotected](https://github.com/flutter/engine/blob/master/lib/ui/isolate_name_server/isolate_name_server.h#L39) which has EXCLUSIVE_LOCKS_REQUIRED(...) annotation. I also tried using the negative annotation but that reports far too many warnings in the existing code which makes it unusable.

I'm fine changing the code, but unless there's a simple workaround I'd still argue for a revert, because the change even if correct has broken an existing usage pattern that worked fine for a long time before and is used in large codebases.

Can you explain in more detail what doesn't work? If you lock mutex_ in the former function, the analysis should register that anyway and don't complain about calling the latter. The LOCK_UNLOCK annotation should be equivalent to EXCLUDES(mutex_) in that regard, too. There should also not be a difference regarding callers of the former function, in both cases the attributes don't propagate.

You should be aware that LOCK_UNLOCK does not prevent double-locking. If that is your concern, use negative annotations, i.e. REQUIRES(!mutex_) and -Wthread-safety-negative. Yes, that likely produces some more warnings, but not outside the class since mutex_ is a private member.

Having LOCK_UNLOCK annotations doesn't seem to be intended, because one should neutralize the other. There was no example in the documentation, and apparently also no test in test/SemaCXX/warn-thread-safety-analysis.cpp. You should use EXCLUDES instead, or if you care enough about avoiding double locking, REQUIRES(!...).

It seems @phosek was able to fix the issue in https://github.com/flutter/engine/pull/5944. By the way, a nice way to think about the attributes is that they encode state transitions as shown in the following table. This change fills the remaining two gaps.

unlockedlocked exclusivelocked shared
unlocked/unknownEXCLUDESACQUIREACQUIRE_SHARED
locked exclusiveRELEASEREQUIRES
locked sharedRELEASE_SHAREDREQUIRES_SHARED

If more people stumble into the issue, another approach would be possible. My understanding is that the order of attributes is preserved. So we could treat ACQUIRE(m) RELEASE(m) = EXCLUDES(m) differently than RELEASE(m) ACQUIRE(m) = REQUIRES(m). But I'm not sure if that's desirable, since normally the order of attributes does not matter.