When a declaration has a cleanup function attached, add it to the CFG. This is basically a destructor on a per-declaration basis.
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
I think this is the right solution and I'm happy to see it!
We have a few "direct" CFG tests in test/Analysis/cfg.c etc.; your patch doesn't seem to be specific to thread safety analysis so it's probably a good idea to add a few direct tests.
clang/lib/Analysis/CFG.cpp | ||
---|---|---|
1945 ↗ | (On Diff #529849) | Is there a way to attach some valid source locations here, like the source location of the attribute's identifier argument? I'm worried that the static analyzer may be unable to consume the CFG without them, it typically requires source locations for almost arbitrary statements. I'm still not sure how it'll react to synthetic statements, maybe turning them into a custom CFGElement subclass (like destructors) would be safer as it'll let all clients of the CFG to treat these in a custom manner. Otherwise it makes it look like a completely normal call-expression to the clients, which is not necessarily a good thing. |
1951–1952 ↗ | (On Diff #529849) | Shouldn't there be a unary operator & here somewhere? In your example the variable is int but the function accepts int *. |
clang/lib/Analysis/CFG.cpp | ||
---|---|---|
1945 ↗ | (On Diff #529849) | Look like I can't query the attribute for any locations except its entire range; I can also just pass VD->getLocation() to all locations here. I thought doing this without creating fake AST nodes is better as well but I wasn't sure how to do that properly, or if I have to update all consumers of the CFG. |
1951–1952 ↗ | (On Diff #529849) | Hm, I think you are right, but it doesn't make difference for the CFG (at least not for thread anysis), does it? Is there a way for me to test that I pass the right parameter value? |
clang/lib/Analysis/CFG.cpp | ||
---|---|---|
1945 ↗ | (On Diff #529849) | Based on the other comment thread, yeah I think it's better to have a new CFG element class than to build synthetic AST. The new element would replace like 5-6 elements that the synthetic AST would otherwise imply. And it'll have the benefit of explicitly indicating that this isn't a "normal" call-expression, which is good because it's likely that many clients actually want *at least a little bit* of custom logic, that they'll otherwise be unable to implement. If you don't want to update all clients, the usual thing to do is to add yet another flag in CFG::BuildOptions, and leave it off by default so that different clients could opt-in gradually as they implement support for the new element. Given that thread-safety analysis is an analysis-based warning, and all analysis-based warnings share the same CFG, you'll probably still have to update the other analysis-based warnings. But there are like 4 of them and all their entry points are in the same function so you can easily see what they are. |
1951–1952 ↗ | (On Diff #529849) |
Hmm wait no that's not how it's supposed to be. The CFG is supposed to have every sub-expression flattened. So if you're adding CallExpr 'foo' `- DeclRefExpr 'x' to the CFG, you're supposed to be adding two elements: one for DeclRefExpr, then one for CallExpr *below* it: [B1] 1. DeclRefExpr 'x' 2. CallExpr 'foo([B1.1])' And if you also add a UnaryOperator: CallExpr 'foo' `- UnaryOperator '&' `- DeclRefExpr 'x' then it's gotta be three CFG elements: [B1] 1. DeclRefExpr 'x' 2. UnaryOperator '&[B1.1]' 3. CallExpr 'foo([B1.2])' This is what clients expect. This probably doesn't matter for thread safety analysis because this DeclRefExpr doesn't have any effect on that analysis, but other clients expect these expressions to be fully flattened this way. (This is especially important in expressions with control flow such as && or ?:.) Typically CallExprs will also have a DeclRefExpr to the callee function as a child, probably with some function to pointer decay, which is a couple more elements. |
Added a new element for cleanup functions.
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
2427 | This handles the function call, but without the instance parameter. I was wondering how to best do that. |
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
2427 | Should you not simply pass SxBuilder.createVariable(CF.getVarDecl()) as third parameter in analogy with the AutomaticObjectDtor case? It might also make sense to copy the attribute check. | |
clang/test/Sema/warn-thread-safety-analysis.c | ||
75–76 | ||
132 | Nitpick: one blank line is enough. |
clang/test/Sema/warn-thread-safety-analysis.c | ||
---|---|---|
75–76 |
|
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
2427 | Can you write a test case that relies on passing the variable? Here is an idea: void unlock_scope(Mutex **mu) __attribute__((release_capability(*mu))) { mutex_exclusive_unlock(*mu); } Mutex* const CLEANUP(unlock_scope) scope = &mu1; mutex_exclusive_lock(*scope); // Unlock should happen automatically. I think this is mildly more interesting than the cleanup function with an unused parameter. Unfortunately this is not quite as powerful as a scoped lock in C++, as we don't track the identity scope == &mu1. So guarded_by won't work with this. But we can at least see warnings on balanced locking/unlocking. As for proper scoped locking, we could treat some variable initializations like construction of a C++ scoped lock. But let's discuss this separately. | |
clang/test/Sema/warn-thread-safety-analysis.c | ||
75–76 | Ah, I didn't know that C gained this only recently. |
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
2427 | Yeah, it doesn't find the lock; I assume that's because the first parameter here is not an _actual_ this parameter; I'd have to handle the var decl as a regular parameter. |
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
2427 | @aaronpuchert Can you explain how that would work? One goal from before was to avoid creating new fake AST nodes, would I'd have to insert the instance parameter as a new DeclRefExpr in the CFG, wouldn't I? |
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
2427 | Ping |
Sorry that it took so long, I had to debug it and think a bit. This should do it:
diff --git a/clang/include/clang/Analysis/Analyses/ThreadSafetyCommon.h b/clang/include/clang/Analysis/Analyses/ThreadSafetyCommon.h index 9d28325c1ea6..13e37ac2b56b 100644 --- a/clang/include/clang/Analysis/Analyses/ThreadSafetyCommon.h +++ b/clang/include/clang/Analysis/Analyses/ThreadSafetyCommon.h @@ -361,7 +361,7 @@ public: unsigned NumArgs = 0; // Function arguments - const Expr *const *FunArgs = nullptr; + llvm::PointerUnion<const Expr *const *, til::SExpr *> FunArgs = nullptr; // is Self referred to with -> or .? bool SelfArrow = false; diff --git a/clang/lib/Analysis/ThreadSafetyCommon.cpp b/clang/lib/Analysis/ThreadSafetyCommon.cpp index b8286cef396c..1eb48c0d5554 100644 --- a/clang/lib/Analysis/ThreadSafetyCommon.cpp +++ b/clang/lib/Analysis/ThreadSafetyCommon.cpp @@ -110,7 +110,8 @@ static StringRef ClassifyDiagnostic(QualType VDT) { /// \param D The declaration to which the attribute is attached. /// \param DeclExp An expression involving the Decl to which the attribute /// is attached. E.g. the call to a function. -/// \param Self S-expression to substitute for a \ref CXXThisExpr. +/// \param Self S-expression to substitute for a \ref CXXThisExpr in a call, +/// or argument to a cleanup function. CapabilityExpr SExprBuilder::translateAttrExpr(const Expr *AttrExp, const NamedDecl *D, const Expr *DeclExp, @@ -144,7 +145,11 @@ CapabilityExpr SExprBuilder::translateAttrExpr(const Expr *AttrExp, if (Self) { assert(!Ctx.SelfArg && "Ambiguous self argument"); - Ctx.SelfArg = Self; + assert(isa<FunctionDecl>(D) && "Self argument requires function"); + if (isa<CXXMethodDecl>(D)) + Ctx.SelfArg = Self; + else + Ctx.FunArgs = Self; // If the attribute has no arguments, then assume the argument is "this". if (!AttrExp) @@ -312,8 +317,14 @@ til::SExpr *SExprBuilder::translateDeclRefExpr(const DeclRefExpr *DRE, ? (cast<FunctionDecl>(D)->getCanonicalDecl() == Canonical) : (cast<ObjCMethodDecl>(D)->getCanonicalDecl() == Canonical)) { // Substitute call arguments for references to function parameters - assert(I < Ctx->NumArgs); - return translate(Ctx->FunArgs[I], Ctx->Prev); + if (const Expr *const *FunArgs = + Ctx->FunArgs.dyn_cast<const Expr *const *>()) { + assert(I < Ctx->NumArgs); + return translate(FunArgs[I], Ctx->Prev); + } else { + assert(I == 0); + return Ctx->FunArgs.get<til::SExpr *>(); + } } } // Map the param back to the param of the original function declaration
The idea (which might be expressed in additional comments) is that until now we only had Self for CXXMethodDecl, but now we also have it for other FunctionDecl, in which case it is to be understood as the sole function argument.
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
1776–1777 | We should relax this like "the implicit this argument or the argument to an implicitly called cleanup function". | |
2427 |
Right, in SExprBuilder::translateAttrExpr we put Self into Ctx.SelfArg, but this is only used for substituting a CXXThisExpr, not a function parameter. |
clang/lib/Analysis/ThreadSafetyCommon.cpp | ||
---|---|---|
320–327 | I'd suggest to go either with const auto * or const Expr *const *. | |
clang/test/Sema/warn-thread-safety-analysis.c | ||
80–84 | FWIW, this might not be so interesting. In the function we don't know that this is being used as a cleanup function, so we just look at it like any function. So we warn. This should already by covered by other tests and has nothing to do with the cleanup attribute. In fact, we probably don't need the definition of any cleanup function, since the function we're interested in is one where it's being used. | |
140 | Would you mind adding a test case that uses substitution? Something like this: void unlock_scope(struct Mutex *const *mu) __attribute__((release_capability(**mu))); void test() { struct Mutex* const __attribute__((cleanup(unlock_scope))) scope = &mu1; mutex_exclusive_lock(scope); // Note that we have to lock through scope, because no alias analysis! // Cleanup happens automatically -> no warning. } This looks like something that might be used in actual C code, maybe hidden behind a macro. In fact, the existing test case doesn't look terribly interesting to me. |
clang/lib/Analysis/ThreadSafety.cpp | ||
---|---|---|
1776–1777 | Would be nice if you could also adapt the comment as stated. |
Side note: I hope you've seen the failing test Analysis/scopes-cfg-output.cpp, but since that belongs to the predecessor change I assume it's an issue there.
Thanks, looks good! I'd like to hear from @aaron.ballman if this warrants a release note, but that can be added separately.
clang/test/Sema/warn-thread-safety-analysis.c | ||
---|---|---|
26 | Oh, I wouldn't mind if you were to drop this spurious added newline. But you can do this when committing, no need for a new patch set. |
We should relax this like "the implicit this argument or the argument to an implicitly called cleanup function".