Skip to content

Thread Safety Analysis: Fix implicit member access in attributes#194457

Merged
melver merged 4 commits into
llvm:mainfrom
melver:for-pull2
May 7, 2026
Merged

Thread Safety Analysis: Fix implicit member access in attributes#194457
melver merged 4 commits into
llvm:mainfrom
melver:for-pull2

Conversation

@melver

@melver melver commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

SExprBuilder previously translated DeclRefExprs referring to FieldDecls as plain global references (til::LiteralPtr), ignoring the base object. This caused false positives when members were accessed implicitly (such as in C, or parameter attributes in C++) because the context of the parent object was lost.

Fix this by using translateCXXThisExpr() to evaluate SelfArg into a base expression when translating a DeclRefExpr to a FieldDecl. This makes the analysis behave correctly for implicit member accesses in attributes.

Assisted-by: Gemini 3 (for debugging and review)

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:analysis labels Apr 27, 2026
@llvmbot

llvmbot commented Apr 27, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang

Author: Marco Elver (melver)

Changes

SExprBuilder previously translated DeclRefExprs referring to FieldDecls as plain global references (til::LiteralPtr), ignoring the base object. This caused false positives when members were accessed implicitly (such as in C, or parameter attributes in C++) because the context of the parent object was lost.

Fix this by using translateCXXThisExpr() to evaluate SelfArg into a base expression when translating a DeclRefExpr to a FieldDecl. This makes the analysis behave correctly for implicit member accesses in attributes.

Assisted-by: Gemini 3 (for debugging and review)


Full diff: https://github.com/llvm/llvm-project/pull/194457.diff

3 Files Affected:

  • (modified) clang/lib/Analysis/ThreadSafetyCommon.cpp (+31-22)
  • (modified) clang/test/Sema/warn-thread-safety-analysis.c (+20-5)
  • (modified) clang/test/SemaCXX/warn-thread-safety-analysis.cpp (+16-8)
diff --git a/clang/lib/Analysis/ThreadSafetyCommon.cpp b/clang/lib/Analysis/ThreadSafetyCommon.cpp
index b43a986521f99..1eabfb6f57071 100644
--- a/clang/lib/Analysis/ThreadSafetyCommon.cpp
+++ b/clang/lib/Analysis/ThreadSafetyCommon.cpp
@@ -374,6 +374,28 @@ til::SExpr *SExprBuilder::translate(const Stmt *S, CallingContext *Ctx) {
   return new (Arena) til::Undefined(S);
 }
 
+static const ValueDecl *getValueDeclFromSExpr(const til::SExpr *E) {
+  if (const auto *V = dyn_cast<til::Variable>(E))
+    return V->clangDecl();
+  if (const auto *Ph = dyn_cast<til::Phi>(E))
+    return Ph->clangDecl();
+  if (const auto *P = dyn_cast<til::Project>(E))
+    return P->clangDecl();
+  if (const auto *L = dyn_cast<til::LiteralPtr>(E))
+    return L->clangDecl();
+  return nullptr;
+}
+
+static bool hasAnyPointerType(const til::SExpr *E) {
+  auto *VD = getValueDeclFromSExpr(E);
+  if (VD && VD->getType()->isAnyPointerType())
+    return true;
+  if (const auto *C = dyn_cast<til::Cast>(E))
+    return C->castOpcode() == til::CAST_objToPtr;
+
+  return false;
+}
+
 til::SExpr *SExprBuilder::translateDeclRefExpr(const DeclRefExpr *DRE,
                                                CallingContext *Ctx) {
   const auto *VD = cast<ValueDecl>(DRE->getDecl()->getCanonicalDecl());
@@ -408,6 +430,15 @@ til::SExpr *SExprBuilder::translateDeclRefExpr(const DeclRefExpr *DRE,
   if (const auto *VarD = dyn_cast<VarDecl>(VD))
     return translateVariable(VarD, Ctx);
 
+  if (const auto *FD = dyn_cast<FieldDecl>(VD); FD && Ctx && Ctx->SelfArg) {
+    til::SExpr *BE = translateCXXThisExpr(nullptr, Ctx);
+    til::SExpr *E = new (Arena) til::SApply(BE);
+    til::Project *P = new (Arena) til::Project(E, FD);
+    if (hasAnyPointerType(BE))
+      P->setArrow(true);
+    return P;
+  }
+
   // For non-local variables, treat it as a reference to a named object.
   return new (Arena) til::LiteralPtr(VD);
 }
@@ -425,28 +456,6 @@ til::SExpr *SExprBuilder::translateCXXThisExpr(const CXXThisExpr *TE,
   return SelfVar;
 }
 
-static const ValueDecl *getValueDeclFromSExpr(const til::SExpr *E) {
-  if (const auto *V = dyn_cast<til::Variable>(E))
-    return V->clangDecl();
-  if (const auto *Ph = dyn_cast<til::Phi>(E))
-    return Ph->clangDecl();
-  if (const auto *P = dyn_cast<til::Project>(E))
-    return P->clangDecl();
-  if (const auto *L = dyn_cast<til::LiteralPtr>(E))
-    return L->clangDecl();
-  return nullptr;
-}
-
-static bool hasAnyPointerType(const til::SExpr *E) {
-  auto *VD = getValueDeclFromSExpr(E);
-  if (VD && VD->getType()->isAnyPointerType())
-    return true;
-  if (const auto *C = dyn_cast<til::Cast>(E))
-    return C->castOpcode() == til::CAST_objToPtr;
-
-  return false;
-}
-
 // Grab the very first declaration of virtual method D
 static const CXXMethodDecl *getFirstVirtualDecl(const CXXMethodDecl *D) {
   while (true) {
diff --git a/clang/test/Sema/warn-thread-safety-analysis.c b/clang/test/Sema/warn-thread-safety-analysis.c
index 0fd382eb02b78..c1fcbace2d29d 100644
--- a/clang/test/Sema/warn-thread-safety-analysis.c
+++ b/clang/test/Sema/warn-thread-safety-analysis.c
@@ -32,6 +32,10 @@
 // Simplified only for test purpose.
 struct LOCKABLE Mutex {};
 
+struct Task {
+  struct Mutex mu;
+};
+
 struct Foo {
   struct Mutex *mu_;
   int  a_value GUARDED_BY(mu_);
@@ -42,6 +46,10 @@ struct Foo {
   } bar;
 
   int* a_ptr PT_GUARDED_BY(bar.other_mu);
+
+  struct Task *task;
+  int  b_value GUARDED_BY(&task->mu);
+  int* b_ptr PT_GUARDED_BY(&task->mu);
 };
 
 struct LOCKABLE Lock {};
@@ -206,22 +214,29 @@ int main(void) {
     a_ = 42;
   }
 
-  foo_.a_value = 0; // expected-warning {{writing variable 'a_value' requires holding mutex 'mu_' exclusively}}
-  *foo_.a_ptr = 1; // expected-warning {{writing the value pointed to by 'a_ptr' requires holding mutex 'bar.other_mu' exclusively}}
-
+  foo_.a_value = 0; // expected-warning {{writing variable 'a_value' requires holding mutex 'foo_.mu_' exclusively}}
+  *foo_.a_ptr = 1; // expected-warning {{writing the value pointed to by 'a_ptr' requires holding mutex 'foo_.bar.other_mu' exclusively}}
+  foo_.b_value = 0; // expected-warning {{writing variable 'b_value' requires holding mutex 'foo_.task->mu' exclusively}}
+  *foo_.b_ptr = 1; // expected-warning {{writing the value pointed to by 'b_ptr' requires holding mutex 'foo_.task->mu' exclusively}}
 
   mutex_exclusive_lock(foo_.bar.other_mu);
+  *foo_.a_ptr = 1;
   mutex_exclusive_lock(foo_.bar.third_mu); // expected-warning{{mutex 'third_mu' must be acquired before 'other_mu'}}
   mutex_exclusive_lock(foo_.mu_); // expected-warning{{mutex 'mu_' must be acquired before 'other_mu'}}
   mutex_exclusive_unlock(foo_.mu_);
   mutex_exclusive_unlock(foo_.bar.other_mu);
   mutex_exclusive_unlock(foo_.bar.third_mu);
 
+  mutex_exclusive_lock(&foo_.task->mu);
+  foo_.b_value = 0;
+  *foo_.b_ptr = 1;
+  mutex_exclusive_unlock(&foo_.task->mu);
+
 #ifdef LATE_PARSING
-  late_parsing.a_value_defined_before = 1; // expected-warning{{writing variable 'a_value_defined_before' requires holding mutex 'a_mutex_defined_late' exclusively}}
+  late_parsing.a_value_defined_before = 1; // expected-warning{{writing variable 'a_value_defined_before' requires holding mutex 'late_parsing.a_mutex_defined_late' exclusively}}
   late_parsing.a_ptr_defined_before = 0;
   set_value(&late_parsing.a_value_defined_before, 0); // expected-warning{{calling function 'set_value' requires holding mutex 'foo_.mu_' exclusively}}
-                                                      // expected-warning@-1{{passing pointer to variable 'a_value_defined_before' requires holding mutex 'a_mutex_defined_late'}}
+                                                      // expected-warning@-1{{passing pointer to variable 'a_value_defined_before' requires holding mutex 'late_parsing.a_mutex_defined_late'}}
   mutex_exclusive_lock(late_parsing.a_mutex_defined_late);
   mutex_exclusive_lock(late_parsing.a_mutex_defined_early); // expected-warning{{mutex 'a_mutex_defined_early' must be acquired before 'a_mutex_defined_late'}}
   mutex_exclusive_unlock(late_parsing.a_mutex_defined_early);
diff --git a/clang/test/SemaCXX/warn-thread-safety-analysis.cpp b/clang/test/SemaCXX/warn-thread-safety-analysis.cpp
index 079e068ca7811..dedf7f9c38427 100644
--- a/clang/test/SemaCXX/warn-thread-safety-analysis.cpp
+++ b/clang/test/SemaCXX/warn-thread-safety-analysis.cpp
@@ -169,10 +169,10 @@ struct TestingMoreComplexAttributes {
 } more_complex_atttributes;
 
 void more_complex_attributes() {
-    more_complex_atttributes.a = true; // expected-warning{{writing variable 'a' requires holding mutex 'lock' exclusively}}
-    more_complex_atttributes.b = true; // expected-warning{{writing variable 'b' requires holding mutex 'strct.lock' exclusively}}
-    *more_complex_atttributes.ptr_a = true; // expected-warning{{writing the value pointed to by 'ptr_a' requires holding mutex 'lock' exclusively}}
-    *more_complex_atttributes.ptr_b = true; // expected-warning{{writing the value pointed to by 'ptr_b' requires holding mutex 'strct.lock' exclusively}}
+    more_complex_atttributes.a = true; // expected-warning{{writing variable 'a' requires holding mutex 'more_complex_atttributes..lock' exclusively}}
+    more_complex_atttributes.b = true; // expected-warning{{writing variable 'b' requires holding mutex 'more_complex_atttributes..strct.lock' exclusively}}
+    *more_complex_atttributes.ptr_a = true; // expected-warning{{writing the value pointed to by 'ptr_a' requires holding mutex 'more_complex_atttributes..lock' exclusively}}
+    *more_complex_atttributes.ptr_b = true; // expected-warning{{writing the value pointed to by 'ptr_b' requires holding mutex 'more_complex_atttributes..strct.lock' exclusively}}
 
     more_complex_atttributes.lock.Lock();
     more_complex_atttributes.lock1.Lock(); // expected-warning{{mutex 'lock1' must be acquired before 'lock'}}
@@ -3739,14 +3739,10 @@ void requireDecl(RelockableScope &scope) {
 struct foo
 {
   Mutex mu;
-  // expected-note@+1{{see attribute on parameter here}}
   void require(RelockableScope &scope EXCLUSIVE_LOCKS_REQUIRED(mu));
   void callRequire(){
     RelockableScope scope(&mu);
-    // TODO: False positive due to incorrect parsing of parameter attribute arguments.
     require(scope);
-    // expected-warning@-1{{calling function 'require' requires holding mutex 'mu' exclusively}}
-    // expected-warning@-2{{mutex managed by 'scope' is 'mu' instead of 'mu'}}
   }
 };
 
@@ -7717,6 +7713,18 @@ void testAliasChainUnrelatedReassignment2(ContainerOfPtr *list) {
   eb->mu.Unlock();
 }
 
+struct GuardedByIndirection {
+  int data GUARDED_BY(foo_ptr->mu);
+  Foo *foo_ptr;
+};
+
+void testGuardedByIndirection(GuardedByIndirection *x) {
+  Foo *f = x->foo_ptr;
+  f->mu.Lock();
+  x->data = 42;
+  f->mu.Unlock();
+}
+
 void testControlFlowDoWhile(Foo *f, int x) {
   Foo *ptr = f;
 
@melver

melver commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author
@melver melver requested a review from AaronBallman May 1, 2026 09:24

@AaronBallman AaronBallman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! LGTM though this should have a release note in clang/docs/ReleaseNotes.rst so users know about the fix.

@aaronpuchert aaronpuchert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be fixed in the AST, and not worked around in the TIL translation. Fields should never be referenced by a DeclRefExpr, but only by MemberExprs. It would also be good to keep a correspondence DeclRefExprtil::LiteralPtr, MemberExprtil::Project.

I know this is an issue in C, but this should better be fixed by introducing a built-in equivalent to CXXThisExpr.

Comment thread clang/test/SemaCXX/warn-thread-safety-analysis.cpp Outdated
@AaronBallman

Copy link
Copy Markdown
Contributor

I think this should be fixed in the AST, and not worked around in the TIL translation. Fields should never be referenced by a DeclRefExpr, but only by MemberExprs. It would also be good to keep a correspondence DeclRefExprtil::LiteralPtr, MemberExprtil::Project.

I know this is an issue in C, but this should better be fixed by introducing a built-in equivalent to CXXThisExpr.

On the one hand, yes (though I would argue it's not "fixed"; C has no such notion so this is purely a language extension at this point); it would make more sense to model this as a kind of member expression. On the other hand, that's a fairly significant amount of effort by comparison. But on the third hand, I think that effort may help in other circumstances like with bounds safety attributes which also want to be able to model fields which can implicitly reference other fields. CC @rapidsna in case there's already been investigation into this approach.

@melver

melver commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

I think this should be fixed in the AST, and not worked around in the TIL translation. Fields should never be referenced by a DeclRefExpr, but only by MemberExprs. It would also be good to keep a correspondence DeclRefExprtil::LiteralPtr, MemberExprtil::Project.
I know this is an issue in C, but this should better be fixed by introducing a built-in equivalent to CXXThisExpr.

On the one hand, yes (though I would argue it's not "fixed"; C has no such notion so this is purely a language extension at this point); it would make more sense to model this as a kind of member expression. On the other hand, that's a fairly significant amount of effort by comparison. But on the third hand, I think that effort may help in other circumstances like with bounds safety attributes which also want to be able to model fields which can implicitly reference other fields. CC @rapidsna in case there's already been investigation into this approach.

Good points, but the AST refactor is probably out of scope for this PR. I've left the TIL fix with a FIXME hinting at the eventual AST-based fix.

This fix here is quite narrow, and unless it interferes with anything else, I'd propose merging is a near-term fix so that users aren't blocked on this - this fix was in response to a report by a Linux kernel maintainer (we already expect needing to bump min. Clang version to use -Wthread-safety to 23, given the various issues that have been fixed after Clang 22's release).

melver and others added 3 commits May 6, 2026 23:20
SExprBuilder previously translated DeclRefExprs referring to FieldDecls
as plain global references (til::LiteralPtr), ignoring the base object.
This caused false positives when members were accessed implicitly (such
as in C, or parameter attributes in C++) because the context of the
parent object was lost.

Fix this by using translateCXXThisExpr() to evaluate SelfArg into a base
expression when translating a DeclRefExpr to a FieldDecl. This makes the
analysis behave correctly for implicit member accesses in attributes.

Assisted-by: Gemini 3 (for debugging and review)
@AaronBallman

Copy link
Copy Markdown
Contributor

This fix here is quite narrow, and unless it interferes with anything else, I'd propose merging is a near-term fix so that users aren't blocked on this - this fix was in response to a report by a Linux kernel maintainer (we already expect needing to bump min. Clang version to use -Wthread-safety to 23, given the various issues that have been fixed after Clang 22's release).

I'm comfortable with that.

@melver melver merged commit dba9696 into llvm:main May 7, 2026
11 checks passed
yebinchon pushed a commit to yebinchon/llvm-project that referenced this pull request May 7, 2026
…m#194457)

SExprBuilder previously translated DeclRefExprs referring to FieldDecls
as plain global references (til::LiteralPtr), ignoring the base object.
This caused false positives when members were accessed implicitly (such
as in C, or parameter attributes in C++) because the context of the
parent object was lost.

Fix this by using translateCXXThisExpr() to evaluate SelfArg into a base
expression when translating a DeclRefExpr to a FieldDecl. This makes the
analysis behave correctly for implicit member accesses in attributes.

Assisted-by: Gemini 3 (for debugging and review)
@rapidsna

Copy link
Copy Markdown
Contributor

Thanks for addressing this issue!

@AaronBallman @aaronpuchert Agreed that the proper fix here is to introduce a notion of this for C, rather than having DeclRefExpr refer to FieldDecl directly. I've filed #197250 to track that work.

moar55 pushed a commit to moar55/llvm-project that referenced this pull request May 12, 2026
…m#194457)

SExprBuilder previously translated DeclRefExprs referring to FieldDecls
as plain global references (til::LiteralPtr), ignoring the base object.
This caused false positives when members were accessed implicitly (such
as in C, or parameter attributes in C++) because the context of the
parent object was lost.

Fix this by using translateCXXThisExpr() to evaluate SelfArg into a base
expression when translating a DeclRefExpr to a FieldDecl. This makes the
analysis behave correctly for implicit member accesses in attributes.

Assisted-by: Gemini 3 (for debugging and review)
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request May 19, 2026
…m#194457)

SExprBuilder previously translated DeclRefExprs referring to FieldDecls
as plain global references (til::LiteralPtr), ignoring the base object.
This caused false positives when members were accessed implicitly (such
as in C, or parameter attributes in C++) because the context of the
parent object was lost.

Fix this by using translateCXXThisExpr() to evaluate SelfArg into a base
expression when translating a DeclRefExpr to a FieldDecl. This makes the
analysis behave correctly for implicit member accesses in attributes.

Assisted-by: Gemini 3 (for debugging and review)
externalmirrors-syncer Bot pushed a commit to external-mirrors/linux-next that referenced this pull request May 21, 2026
Clang 23 introduces several major improvements:

1. Support for multiple arguments in the `guarded_by` and
   `pt_guarded_by` attributes [1]. This allows defining variables
   protected by multiple context locks, where read access requires
   holding at least one lock (shared or exclusive), and write access
   requires holding all of them exclusively.

2. Function pointer support [2]. We can now add attributes to function
   pointers just like we do on normal functions.

3. A fix to use arrays of locks [3]. Each index is now correctly treated
   as a separate lock instance.

4. A fix for implicit member access in attributes [4]. This allows to
   use __guarded_by(&foo->lock) correctly.

Overall that makes it worthwhile bumping the compiler version instead of
trying to make both Clang 22 and later work while supporting these new
features.

Signed-off-by: Marco Elver <elver@google.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Reviewed-by: Nathan Chancellor <nathan@kernel.org>
Reviewed-by: Bart Van Assche <bvanassche@acm.org>
Link: llvm/llvm-project#186838 [1]
Link: llvm/llvm-project#191187 [2]
Link: llvm/llvm-project#148551 [3]
Link: llvm/llvm-project#194457 [4]
Link: https://patch.msgid.link/20260515124426.2227783-1-elver@google.com
mj22226 pushed a commit to mj22226/linux that referenced this pull request May 30, 2026
Clang 23 introduces several major improvements:

1. Support for multiple arguments in the `guarded_by` and
   `pt_guarded_by` attributes [1]. This allows defining variables
   protected by multiple context locks, where read access requires
   holding at least one lock (shared or exclusive), and write access
   requires holding all of them exclusively.

2. Function pointer support [2]. We can now add attributes to function
   pointers just like we do on normal functions.

3. A fix to use arrays of locks [3]. Each index is now correctly treated
   as a separate lock instance.

4. A fix for implicit member access in attributes [4]. This allows to
   use __guarded_by(&foo->lock) correctly.

Overall that makes it worthwhile bumping the compiler version instead of
trying to make both Clang 22 and later work while supporting these new
features.

Signed-off-by: Marco Elver <elver@google.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Reviewed-by: Nathan Chancellor <nathan@kernel.org>
Reviewed-by: Bart Van Assche <bvanassche@acm.org>
Link: llvm/llvm-project#186838 [1]
Link: llvm/llvm-project#191187 [2]
Link: llvm/llvm-project#148551 [3]
Link: llvm/llvm-project#194457 [4]
Link: https://patch.msgid.link/20260515124426.2227783-1-elver@google.com
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:analysis clang Clang issues not falling into any other category

5 participants