This is an archive of the discontinued LLVM Phabricator instance.

[clang][ThreadSafety] Analyze cleanup functions
ClosedPublic

Authored by tbaeder on Jun 9 2023, 12:57 AM.

Details

Summary

When a declaration has a cleanup function attached, add it to the CFG. This is basically a destructor on a per-declaration basis.

Diff Detail

Event Timeline

tbaeder created this revision.Jun 9 2023, 12:57 AM
Herald added a project: Restricted Project. · View Herald TranscriptJun 9 2023, 12:57 AM
tbaeder requested review of this revision.Jun 9 2023, 12:57 AM
Herald added a project: Restricted Project. · View Herald TranscriptJun 9 2023, 12:57 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript
NoQ added a comment.Jun 9 2023, 3:21 PM

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
1949

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.

1955–1956

Shouldn't there be a unary operator & here somewhere? In your example the variable is int but the function accepts int *.

tbaeder updated this revision to Diff 530175.Jun 10 2023, 12:25 AM
tbaeder added inline comments.
clang/lib/Analysis/CFG.cpp
1949

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.

1955–1956

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?

tbaeder added a comment.EditedJun 19 2023, 1:37 AM

Ping

NoQ added inline comments.Jun 20 2023, 3:26 PM
clang/lib/Analysis/CFG.cpp
1949

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.

1955–1956

it doesn't make difference for the CFG

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.

tbaeder updated this revision to Diff 533175.Jun 21 2023, 1:29 AM

Added a new element for cleanup functions.

clang/lib/Analysis/ThreadSafety.cpp
2436

This handles the function call, but without the instance parameter. I was wondering how to best do that.

aaronpuchert added inline comments.
clang/lib/Analysis/ThreadSafety.cpp
2436

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
76–77
136

Nitpick: one blank line is enough.

tbaeder marked 2 inline comments as done.Jun 27 2023, 7:29 AM
tbaeder added inline comments.
clang/test/Sema/warn-thread-safety-analysis.c
76–77

omitting the parameter name in a function definition is a C2x extension

tbaeder updated this revision to Diff 534982.Jun 27 2023, 7:36 AM
tbaeder marked an inline comment as done.
tbaeder marked an inline comment as done.
aaronpuchert added inline comments.Jun 27 2023, 12:35 PM
clang/lib/Analysis/ThreadSafety.cpp
2436

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
76–77

Ah, I didn't know that C gained this only recently.

tbaeder added inline comments.Jun 28 2023, 6:09 AM
clang/lib/Analysis/ThreadSafety.cpp
2436

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.

tbaeder added inline comments.Jul 10 2023, 8:24 AM
clang/lib/Analysis/ThreadSafety.cpp
2436

@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?

tbaeder added inline comments.Jul 17 2023, 8:57 AM
clang/lib/Analysis/ThreadSafety.cpp
2436

Ping

tbaeder abandoned this revision.Aug 17 2023, 12:24 AM

I split out the CFG parts as https://reviews.llvm.org/D152504

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
1777

We should relax this like "the implicit this argument or the argument to an implicitly called cleanup function".

2436

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.

Right, in SExprBuilder::translateAttrExpr we put Self into Ctx.SelfArg, but this is only used for substituting a CXXThisExpr, not a function parameter.

tbaeder updated this revision to Diff 556399.Sep 11 2023, 1:09 AM
aaronpuchert added inline comments.Sep 11 2023, 4:27 PM
clang/lib/Analysis/ThreadSafetyCommon.cpp
320 ↗(On Diff #556399)

I'd suggest to go either with const auto * or const Expr *const *.

clang/test/Sema/warn-thread-safety-analysis.c
81–85

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.

146

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.

tbaeder updated this revision to Diff 556535.Sep 12 2023, 1:52 AM
tbaeder marked 3 inline comments as done.
aaronpuchert accepted this revision.Sep 14 2023, 4:47 PM
aaronpuchert added inline comments.
clang/lib/Analysis/ThreadSafety.cpp
1777

Would be nice if you could also adapt the comment as stated.

This revision is now accepted and ready to land.Sep 14 2023, 4:47 PM

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.

tbaeder updated this revision to Diff 556838.Sep 15 2023, 2:39 AM
tbaeder marked 2 inline comments as done.Sep 15 2023, 2:53 AM
aaronpuchert accepted this revision.Sep 16 2023, 12:45 PM

Thanks, looks good! I'd like to hear from @aaron.ballman if this warrants a release note, but that can be added separately.

aaronpuchert added inline comments.Sep 16 2023, 12:47 PM
clang/test/Sema/warn-thread-safety-analysis.c
27

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.

This revision was automatically updated to reflect the committed changes.
tbaeder marked an inline comment as done.