[clang] Fix thread_local incorrectly treated as tentative definition (fixes #217636) (PR #219780)

ahmed mohamed kamel via cfe-commits cfe-commits at lists.llvm.org
Sun Oct 4 15:28:46 PDT 2026


https://github.com/AhmedKamel10 updated https://github.com/llvm/llvm-project/pull/219780

>From 5eaf3f6e46cefc81bcb5bf60b0f48d0e784f16e0 Mon Sep 17 00:00:00 2001
From: ahmedkamel10 <amkzaher at gmail.com>
Date: Sun, 30 Aug 2026 09:50:50 +0300
Subject: [PATCH] [Clang] Prevent thread_local from creating tentative
 definitions (#217636)

---
 clang/lib/AST/Decl.cpp                         | 18 +++++++++++-------
 clang/lib/Sema/SemaDecl.cpp                    |  8 +++++++-
 .../test/CodeGen/PowerPC/toc-data-attribute.c  |  3 +--
 .../CodeGen/PowerPC/toc-data-diagnostics.c     |  2 +-
 clang/test/CodeGen/thread-specifier.c          |  4 ++--
 clang/test/Sema/PowerPC/aix-attr-tls_model.c   |  8 ++++----
 clang/test/Sema/attr-tls_model.c               | 10 ++++++----
 clang/test/Sema/tentative-decls.c              |  3 +++
 8 files changed, 35 insertions(+), 21 deletions(-)

diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index b145560508e95..afc53ff5cc3f2 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -2309,16 +2309,20 @@ VarDecl::isThisDeclarationADefinition(ASTContext &C) const {
   if (isSingleLineLanguageLinkage(*this))
     return DeclarationOnly;
 
-  // C99 6.9.2p2:
-  //   A declaration of an object that has file scope without an initializer,
-  //   and without a storage class specifier or the scs 'static', constitutes
-  //   a tentative definition.
+  // C23 6.9.3p2:
+  //   A declaration of an identifier for an object that has file scope
+  //   without an initializer, and without the storage-class specifier extern
+  //   or thread_local, constitutes a tentative definition.
+  // File-scope thread_local objects are instead external definitions (C23
+  // 6.9.3p1). Earlier standards have no thread_local, but we apply the rule
+  // in every C mode, including for __thread.
   // No such thing in C++.
-  if (!C.getLangOpts().CPlusPlus && isFileVarDecl())
+  if (!C.getLangOpts().CPlusPlus && isFileVarDecl() && getTLSKind() == TLS_None)
     return TentativeDefinition;
 
-  // What's left is (in C, block-scope) declarations without initializers or
-  // external storage. These are definitions.
+  // What's left is (in C) block-scope declarations and file-scope thread_local
+  // declarations without initializers or external storage. These are
+  // definitions.
   return Definition;
 }
 
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 094601a58d508..dbf623a4874ac 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -4976,7 +4976,13 @@ void Sema::MergeVarDecl(VarDecl *New, LookupResult &Previous) {
     // [basic.def]p2 for details, but the basic idea is: if the old declaration
     // contains the extern specifier and doesn't have an initializer, it's fine
     // in C++.
-    if (Old->getStorageClass() != SC_Extern || Old->hasInit()) {
+    if (New->getTLSKind() != VarDecl::TLS_None &&
+        New->isThisDeclarationADefinition() == VarDecl::Definition) {
+      VarDecl *Def = Old->getDefinition();
+      if (Def && checkVarDeclRedefinition(Def, New)) {
+        return;
+      }
+    } else if (Old->getStorageClass() != SC_Extern || Old->hasInit()) {
       Diag(New->getLocation(), diag::warn_cxx_compat_tentative_definition)
           << New;
       Diag(Old->getLocation(), diag::note_previous_declaration);
diff --git a/clang/test/CodeGen/PowerPC/toc-data-attribute.c b/clang/test/CodeGen/PowerPC/toc-data-attribute.c
index d3a2d43b064ff..7c2459cb2588f 100644
--- a/clang/test/CodeGen/PowerPC/toc-data-attribute.c
+++ b/clang/test/CodeGen/PowerPC/toc-data-attribute.c
@@ -41,10 +41,9 @@ struct SomeStruct* getAddress(void) {
 // CHECK32: @l = global double 2.500000e+00, align 8
 // CHECK64: @l = global double 2.500000e+00, align 8 #0
 // COMMON: @m = global i32 10, section "foo", align 4
+// COMMON: @n = thread_local global i32 0, align 4
 // COMMON: @f = external global i32, align 4 #0
 // COMMON: @o = external global %struct.SomeStruct, align 1
 // CHECK32: @i = global ptr null, align 4 #0
 // CHECK64: @i = global ptr null, align 8 #0
-// COMMON: @n = thread_local global i32 0, align 4
 // COMMON: @p = external global [0 x i32], align 4
-// COMMON: attributes #0 = { "toc-data" }
diff --git a/clang/test/CodeGen/PowerPC/toc-data-diagnostics.c b/clang/test/CodeGen/PowerPC/toc-data-diagnostics.c
index 169c355136993..77e9d399288c0 100644
--- a/clang/test/CodeGen/PowerPC/toc-data-diagnostics.c
+++ b/clang/test/CodeGen/PowerPC/toc-data-diagnostics.c
@@ -56,10 +56,10 @@ long long test2() {
 // CHECK: @c = global i32 0, align 128
 // CHECK: @d = global double 2.500000e+00, align 8
 // CHECK: @e = global i32 10, section "foo", align 4
+// CHECK: @f = thread_local global i32 0, align 4
 // CHECK: @globalOneWithAlias = global i32 10, align 4
 // CHECK: @globalTwoWithAlias = global i32 20, align 4
 // CHECK: @a = global %struct.large_struct zeroinitializer, align 4
-// CHECK: @f = thread_local global i32 0, align 4
 // CHECK: @h = external global [0 x i32], align 4
 // CHECK: @g = external global %struct.SomeStruct, align 1 
 // CHECK: @test2.ll = internal global i64 5, align 8
diff --git a/clang/test/CodeGen/thread-specifier.c b/clang/test/CodeGen/thread-specifier.c
index 18a14f912aba4..ed8542cafa92d 100644
--- a/clang/test/CodeGen/thread-specifier.c
+++ b/clang/test/CodeGen/thread-specifier.c
@@ -2,15 +2,15 @@
 // RUN: %clang_cc1 -triple riscv32 -emit-llvm -o - %s | FileCheck %s
 // RUN: %clang_cc1 -triple riscv64 -emit-llvm -o - %s | FileCheck %s
 
+// CHECK: @a ={{.*}} thread_local global
 // CHECK: @b = external thread_local global
 // CHECK: @d.e = internal thread_local global
 // CHECK: @d.f = internal thread_local global
-// CHECK: @f.a = internal thread_local(initialexec) global
-// CHECK: @a ={{.*}} thread_local global
 // CHECK: @g ={{.*}} thread_local global
 // CHECK: @h ={{.*}} thread_local(localdynamic) global
 // CHECK: @i ={{.*}} thread_local(initialexec) global
 // CHECK: @j ={{.*}} thread_local(localexec) global
+// CHECK: @f.a = internal thread_local(initialexec) global
 
 // CHECK-NOT: @_ZTW
 // CHECK-NOT: @_ZTH
diff --git a/clang/test/Sema/PowerPC/aix-attr-tls_model.c b/clang/test/Sema/PowerPC/aix-attr-tls_model.c
index 7c2047bced939..616f5526932fd 100644
--- a/clang/test/Sema/PowerPC/aix-attr-tls_model.c
+++ b/clang/test/Sema/PowerPC/aix-attr-tls_model.c
@@ -5,7 +5,7 @@
 #error "Should support tls_model attribute"
 #endif
 
-static __thread int y __attribute((tls_model("global-dynamic"))); // no-warning
-static __thread int y __attribute((tls_model("local-dynamic"))); // expected-no-diagnostics
-static __thread int y __attribute((tls_model("initial-exec"))); // no-warning
-static __thread int y __attribute((tls_model("local-exec"))); // no-warning
+static __thread int y_gd __attribute((tls_model("global-dynamic"))); // no-warning
+static __thread int y_ld __attribute((tls_model("local-dynamic"))); // expected-no-diagnostics
+static __thread int y_ie __attribute((tls_model("initial-exec"))); // no-warning
+static __thread int y_le __attribute((tls_model("local-exec"))); // no-warning
\ No newline at end of file
diff --git a/clang/test/Sema/attr-tls_model.c b/clang/test/Sema/attr-tls_model.c
index ec99c7aacaa3a..dfc94e663ea13 100644
--- a/clang/test/Sema/attr-tls_model.c
+++ b/clang/test/Sema/attr-tls_model.c
@@ -7,8 +7,10 @@
 int f(void) __attribute((tls_model("global-dynamic"))); // expected-error {{'tls_model' attribute only applies to thread-local variables}}
 
 int x __attribute((tls_model("global-dynamic"))); // expected-error {{'tls_model' attribute only applies to thread-local variables}}
-static __thread int y __attribute((tls_model("global-dynamic"))); // no-warning
+static __thread int y_ok __attribute((tls_model("global-dynamic"))); // no-warning
+
+static __thread int y_two_args __attribute((tls_model("local", "dynamic"))); // expected-error {{'tls_model' attribute takes one argument}}
+static __thread int y_not_str __attribute((tls_model(123))); // expected-error {{expected string literal as argument of 'tls_model' attribute}}
+static __thread int y_bad_model __attribute((tls_model("foobar"))); // expected-error {{tls_model must be "global-dynamic", "local-dynamic", "initial-exec" or "local-exec"}}
+
 
-static __thread int y __attribute((tls_model("local", "dynamic"))); // expected-error {{'tls_model' attribute takes one argument}}
-static __thread int y __attribute((tls_model(123))); // expected-error {{expected string literal as argument of 'tls_model' attribute}}
-static __thread int y __attribute((tls_model("foobar"))); // expected-error {{tls_model must be "global-dynamic", "local-dynamic", "initial-exec" or "local-exec"}}
diff --git a/clang/test/Sema/tentative-decls.c b/clang/test/Sema/tentative-decls.c
index 94d21bdbf94da..3d2dd147905cc 100644
--- a/clang/test/Sema/tentative-decls.c
+++ b/clang/test/Sema/tentative-decls.c
@@ -63,3 +63,6 @@ static int b0;
 
 static int a0[] = { 4 };
 static int b0 = 5;
+
+_Thread_local int tls_redef; // expected-note {{previous definition is here}}
+_Thread_local int tls_redef; // expected-error {{redefinition of 'tls_redef'}}



More information about the cfe-commits mailing list