[clang] [clang] Fix thread_local incorrectly treated as tentative definition (PR #219780)
ahmed mohamed kamel via cfe-commits
cfe-commits at lists.llvm.org
Tue Oct 6 08:54:10 PDT 2026
https://github.com/AhmedKamel10 updated https://github.com/llvm/llvm-project/pull/219780
>From 3fc9fd22b4ad358fd37b66550170fb49792d0923 Mon Sep 17 00:00:00 2001
From: ahmedkamel10 <amkzaher at gmail.com>
Date: Sun, 30 Aug 2026 09:50:50 +0300
Subject: [PATCH 1/2] [Clang] Prevent thread_local from creating tentative
definitions (#217636)
---
clang/lib/AST/Decl.cpp | 21 +++++++++-------
clang/lib/Sema/SemaDecl.cpp | 8 ++++++-
clang/test/Sema/c23-thread-local-definition.c | 24 +++++++++++++++++++
3 files changed, 44 insertions(+), 9 deletions(-)
create mode 100644 clang/test/Sema/c23-thread-local-definition.c
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index b145560508e956..a4f7560db19ca3 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -2309,16 +2309,21 @@ 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.
- // No such thing in C++.
- if (!C.getLangOpts().CPlusPlus && isFileVarDecl())
+ // 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.
+ // In C23 and later, file-scope thread_local / _Thread_local / __thread
+ // declarations without initializers are full external definitions
+ // (C23 6.9.3p1). Pre-C23 standards allowed tentative TLS definitions, so we
+ // preserve that behavior in earlier language modes.
+ if (!C.getLangOpts().CPlusPlus && isFileVarDecl() &&
+ (getTLSKind() == TLS_None || !C.getLangOpts().C23))
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, in C23, 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 094601a58d5085..dbf623a4874acd 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/Sema/c23-thread-local-definition.c b/clang/test/Sema/c23-thread-local-definition.c
new file mode 100644
index 00000000000000..cb5aeea2feb40e
--- /dev/null
+++ b/clang/test/Sema/c23-thread-local-definition.c
@@ -0,0 +1,24 @@
+// RUN: %clang_cc1 -std=c23 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -std=c17 -fsyntax-only -verify=pre23 %s
+// pre23-no-diagnostics
+
+_Thread_local int a; // expected-note {{previous definition is here}}
+_Thread_local int a; // expected-error {{redefinition of 'a'}}
+
+static _Thread_local int b; // expected-note {{previous definition is here}}
+static _Thread_local int b; // expected-error {{redefinition of 'b'}}
+
+__thread int c; // expected-note {{previous definition is here}}
+__thread int c; // expected-error {{redefinition of 'c'}}
+
+// 'extern' makes these declarations, not definitions.
+extern _Thread_local int e;
+_Thread_local int e;
+
+_Thread_local int f;
+extern _Thread_local int f;
+
+#if __STDC_VERSION__ >= 202311L
+thread_local int x, x; // expected-error {{redefinition of 'x'}} \
+ // expected-note {{previous definition is here}}
+#endif
>From abecb99fa87e0da49450e970a83f2b99ef76ab52 Mon Sep 17 00:00:00 2001
From: ahmedkamel10 <amkzaher at gmail.com>
Date: Tue, 6 Oct 2026 18:53:47 +0300
Subject: [PATCH 2/2] Add release note
---
clang/docs/ReleaseNotes.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 7ec126a065ae5a..ba13120cb10756 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -603,7 +603,8 @@ features cannot lower the translation-unit ABI level;
- No longer crashing due to follow-on diagnostics when there is an invalid operand in a logical operator involving a vector operand. (#GH227588)
- Fixed assertion failures caused by stale linkage information when an extern variable or function declaration is merged with a preceding static declaration. (#GH204759, #GH204754)
- Fixed a crash due to typo correction mishandling custom keywords `_virtual_inheritance` and `_multiple_inheritance` in `-fms-compatibility` mode. (#GH228003)
-
+- Clang no longer treats a file-scope `thread_local` declaration without an initializer as a tentative definition in C23 mode. As specified by C23 6.9.3, such a declaration is a definition, so declaring the same variable more that once is now diagnosed as a redefinition. (#GH217636)
+
#### Bug Fixes to Compiler Builtins
- Fixed a crash when classifying a call to a builtin with dependent arguments,
More information about the cfe-commits
mailing list