[clang] [Clang] Diagnose UB and emit error when identifier has both internal and external linkage (PR #192116)
Aditya Medhane via cfe-commits
cfe-commits at lists.llvm.org
Sun Apr 19 01:06:04 PDT 2026
https://github.com/flash1729 updated https://github.com/llvm/llvm-project/pull/192116
>From 61ee18fbf4a83acdcfa749c6f7049403fa293687 Mon Sep 17 00:00:00 2001
From: flash1729 <sherlockedaditya at gmail.com>
Date: Wed, 15 Apr 2026 00:42:00 +0530
Subject: [PATCH 1/4] [Clang] Diagnose UB when identifier has both internal and
external linkage
C11 6.2.2p7 states that if the same identifier appears with both internal
and external linkage within a translation unit, the behavior is undefined.
This occurs when a block-scope extern declaration finds a file-scope static
declaration, but a no-linkage local variable shadows the static, preventing
linkage inheritance per C11 6.2.2p4.
Fixes #54215
---
clang/docs/ReleaseNotes.rst | 4 +++
.../clang/Basic/DiagnosticSemaKinds.td | 3 ++
clang/lib/Sema/SemaDecl.cpp | 18 ++++++++++
clang/test/Sema/linkage-internal-extern.c | 36 +++++++++++++++++++
4 files changed, 61 insertions(+)
create mode 100644 clang/test/Sema/linkage-internal-extern.c
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index fd58d7847717c..f1efd5f8c3b35 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -366,6 +366,10 @@ Improvements to Clang's diagnostics
code can automatically be made portable to other host platforms that don't
support backslashes.
+- Clang now diagnoses the undefined behavior described in C11 6.2.2p7 where
+ the same identifier appears with both internal and external linkage within a
+ translation unit. (#GH54215)
+
Improvements to Clang's time-trace
----------------------------------
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 6d2fae551566f..8981d30475dde 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -6528,6 +6528,9 @@ def err_inline_declaration_block_scope : Error<
"inline declaration of %0 not allowed in block scope">;
def err_static_non_static : Error<
"static declaration of %0 follows non-static declaration">;
+def err_internal_extern_mismatch : Error<
+ "variable %0 declared with both internal and external linkage "
+ "in the same translation unit; behavior is undefined">;
def err_different_language_linkage : Error<
"declaration of %0 has a different language linkage">;
def ext_retained_language_linkage : Extension<
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 9f8fc5a187b0a..19cc36446eedb 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -4800,6 +4800,24 @@ void Sema::MergeVarDecl(VarDecl *New, LookupResult &Previous) {
return New->setInvalidDecl();
}
}
+
+ // C11 6.2.2p7:
+ // If, within a translation unit, the same identifier appears with both
+ // internal and external linkage, the behavior is undefined.
+ //
+ // This can occur when an extern declaration in block scope finds a file-scope
+ // static declaration, but a no-linkage local variable shadowed the static,
+ // preventing linkage inheritance per C11 6.2.2p4.
+ if (New->isLocalVarDecl() && New->hasExternalStorage() &&
+ Old->isFileVarDecl() && Old->hasLinkage() &&
+ Previous.isShadowed() &&
+ Old->getFormalLinkage() == Linkage::Internal) {
+ Diag(New->getLocation(), diag::err_internal_extern_mismatch)
+ << New->getDeclName();
+ Diag(OldLocation, PrevDiag);
+ return New->setInvalidDecl();
+ }
+
// C99 6.2.2p4:
// For an identifier declared with the storage-class specifier
// extern in a scope in which a prior declaration of that
diff --git a/clang/test/Sema/linkage-internal-extern.c b/clang/test/Sema/linkage-internal-extern.c
new file mode 100644
index 0000000000000..42591c5b21f0b
--- /dev/null
+++ b/clang/test/Sema/linkage-internal-extern.c
@@ -0,0 +1,36 @@
+// RUN: %clang_cc1 -verify -fsyntax-only %s
+
+// C11 6.2.2p7: same identifier with both internal and external linkage is UB.
+
+// Conflicting linkage (UB)
+
+static int x; // expected-note {{previous}}
+void test_basic_shadow(void) {
+ int x;
+ { extern int x; } // expected-error {{declared with both internal and external linkage}}
+}
+
+static int y; // expected-note {{previous}}
+void test_deep_nesting(void) {
+ int y;
+ { int y; { { extern int y; } } } // expected-error {{declared with both internal and external linkage}}
+}
+
+static int p; // expected-note {{previous}}
+void test_param_shadow(int p) {
+ { extern int p; } // expected-error {{declared with both internal and external linkage}}
+}
+
+// Valid cases
+
+static int a;
+void test_no_shadow(void) {
+ extern int a;
+}
+
+void test_no_file_scope(void) {
+ for (static int b = 0;;) {
+ extern int b;
+ break;
+ }
+}
>From c06f72132c84a5221c18f03ea640dd7b61825967 Mon Sep 17 00:00:00 2001
From: flash1729 <sherlockedaditya at gmail.com>
Date: Wed, 15 Apr 2026 01:07:53 +0530
Subject: [PATCH 2/4] fixup! [Clang] Diagnose UB when identifier has both
internal and external linkage
---
clang/lib/Sema/SemaDecl.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 19cc36446eedb..bfe7d8cae888a 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -4809,8 +4809,7 @@ void Sema::MergeVarDecl(VarDecl *New, LookupResult &Previous) {
// static declaration, but a no-linkage local variable shadowed the static,
// preventing linkage inheritance per C11 6.2.2p4.
if (New->isLocalVarDecl() && New->hasExternalStorage() &&
- Old->isFileVarDecl() && Old->hasLinkage() &&
- Previous.isShadowed() &&
+ Old->isFileVarDecl() && Old->hasLinkage() && Previous.isShadowed() &&
Old->getFormalLinkage() == Linkage::Internal) {
Diag(New->getLocation(), diag::err_internal_extern_mismatch)
<< New->getDeclName();
>From 6e78e961340bf9e86b3b99863911e4c21a4e7c57 Mon Sep 17 00:00:00 2001
From: flash1729 <sherlockedaditya at gmail.com>
Date: Wed, 15 Apr 2026 01:39:21 +0530
Subject: [PATCH 3/4] fixup! [Clang] Diagnose UB when identifier has both
internal and external linkage
---
clang/lib/Sema/SemaDecl.cpp | 6 +++---
clang/test/C/C2y/n3410.c | 8 +++-----
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index bfe7d8cae888a..26bd18ade5348 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -4808,9 +4808,9 @@ void Sema::MergeVarDecl(VarDecl *New, LookupResult &Previous) {
// This can occur when an extern declaration in block scope finds a file-scope
// static declaration, but a no-linkage local variable shadowed the static,
// preventing linkage inheritance per C11 6.2.2p4.
- if (New->isLocalVarDecl() && New->hasExternalStorage() &&
- Old->isFileVarDecl() && Old->hasLinkage() && Previous.isShadowed() &&
- Old->getFormalLinkage() == Linkage::Internal) {
+ if (!getLangOpts().CPlusPlus && New->isLocalVarDecl() &&
+ New->hasExternalStorage() && Old->isFileVarDecl() && Old->hasLinkage() &&
+ Previous.isShadowed() && Old->getFormalLinkage() == Linkage::Internal) {
Diag(New->getLocation(), diag::err_internal_extern_mismatch)
<< New->getDeclName();
Diag(OldLocation, PrevDiag);
diff --git a/clang/test/C/C2y/n3410.c b/clang/test/C/C2y/n3410.c
index e1cb41f375b82..c7162f6d324d8 100644
--- a/clang/test/C/C2y/n3410.c
+++ b/clang/test/C/C2y/n3410.c
@@ -24,19 +24,17 @@ void func2() {
extern int b; // Ok
}
-static int c, d;
+static int c, d; // expected-note {{previous definition is here}} expected-note {{previous definition is here}}
void func3() {
int c; // no linkage, different object from the one declared above.
for (int d;;) {
// This 'c' is the same as the one declared at file scope, but because of
// the local scope 'c', the file scope 'c' is not visible.
- // FIXME: This should be diagnosed under N3410.
- extern int c;
+ extern int c; // expected-error {{declared with both internal and external linkage}}
// This 'd' is the same as the one declared at file scope as well, but
// because of the 'd' declared within the for loop, the file scope 'd' is
// also not visible, same as with 'c'.
- // FIXME: This should be diagnosed under N3410.
- extern int d;
+ extern int d; // expected-error {{declared with both internal and external linkage}}
}
for (static int e;;) {
extern int e; // Ok for the same reason as 'b' above.
>From d5cf291cb982df1d1277a46dea45ad8f71678aff Mon Sep 17 00:00:00 2001
From: flash1729 <sherlockedaditya at gmail.com>
Date: Sun, 19 Apr 2026 13:30:18 +0530
Subject: [PATCH 4/4] fixup! [Clang] Diagnose UB when identifier has both
internal and external linkage
---
clang/docs/ReleaseNotes.rst | 9 ++++++---
.../clang/Basic/DiagnosticSemaKinds.td | 2 +-
clang/lib/Sema/SemaDecl.cpp | 10 ++++++----
clang/test/C/C2y/n3410.c | 2 +-
clang/test/Sema/linkage-internal-extern.cpp | 19 +++++++++++++++++++
clang/www/c_status.html | 2 +-
6 files changed, 34 insertions(+), 10 deletions(-)
create mode 100644 clang/test/Sema/linkage-internal-extern.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index f1efd5f8c3b35..715e8f00a4676 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -147,6 +147,12 @@ C Language Changes
C2y Feature Support
^^^^^^^^^^^^^^^^^^^
+- Clang now diagnoses the use of the same identifier with both internal and
+ external linkage within a translation unit, as made ill-formed by
+ `N3410 <https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3410.pdf>`_.
+ This is also diagnosed in older C language modes as the behavior was
+ undefined prior to C2y. (#GH54215)
+
C23 Feature Support
^^^^^^^^^^^^^^^^^^^
- Clang now allows C23 ``constexpr`` struct member access through the dot operator in constant expressions. (#GH178349)
@@ -366,9 +372,6 @@ Improvements to Clang's diagnostics
code can automatically be made portable to other host platforms that don't
support backslashes.
-- Clang now diagnoses the undefined behavior described in C11 6.2.2p7 where
- the same identifier appears with both internal and external linkage within a
- translation unit. (#GH54215)
Improvements to Clang's time-trace
----------------------------------
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 8981d30475dde..03c43f9431df6 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -6530,7 +6530,7 @@ def err_static_non_static : Error<
"static declaration of %0 follows non-static declaration">;
def err_internal_extern_mismatch : Error<
"variable %0 declared with both internal and external linkage "
- "in the same translation unit; behavior is undefined">;
+ "in the same translation unit%select{; behavior is undefined|}1">;
def err_different_language_linkage : Error<
"declaration of %0 has a different language linkage">;
def ext_retained_language_linkage : Extension<
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 26bd18ade5348..ee371466fbb75 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -4801,18 +4801,20 @@ void Sema::MergeVarDecl(VarDecl *New, LookupResult &Previous) {
}
}
- // C11 6.2.2p7:
+ // C2y 6.2.2p7 (N3410):
// If, within a translation unit, the same identifier appears with both
- // internal and external linkage, the behavior is undefined.
+ // internal and external linkage, the program is ill-formed.
+ //
+ // In C11 through C23, this was undefined behavior (C11 6.2.2p7).
//
// This can occur when an extern declaration in block scope finds a file-scope
// static declaration, but a no-linkage local variable shadowed the static,
- // preventing linkage inheritance per C11 6.2.2p4.
+ // preventing linkage inheritance per C2y 6.2.2p4.
if (!getLangOpts().CPlusPlus && New->isLocalVarDecl() &&
New->hasExternalStorage() && Old->isFileVarDecl() && Old->hasLinkage() &&
Previous.isShadowed() && Old->getFormalLinkage() == Linkage::Internal) {
Diag(New->getLocation(), diag::err_internal_extern_mismatch)
- << New->getDeclName();
+ << New->getDeclName() << getLangOpts().C2y;
Diag(OldLocation, PrevDiag);
return New->setInvalidDecl();
}
diff --git a/clang/test/C/C2y/n3410.c b/clang/test/C/C2y/n3410.c
index c7162f6d324d8..27baf9b44458a 100644
--- a/clang/test/C/C2y/n3410.c
+++ b/clang/test/C/C2y/n3410.c
@@ -1,6 +1,6 @@
// RUN: %clang_cc1 -verify -std=c2y -Wall -pedantic -Wno-unused %s
-/* WG14 N3410: No
+/* WG14 N3410: Yes
* Slay Some Earthly Demons XI
*
* It is now ill-formed for the same identifier within a TU to have both
diff --git a/clang/test/Sema/linkage-internal-extern.cpp b/clang/test/Sema/linkage-internal-extern.cpp
new file mode 100644
index 0000000000000..ab084767305fd
--- /dev/null
+++ b/clang/test/Sema/linkage-internal-extern.cpp
@@ -0,0 +1,19 @@
+// RUN: %clang_cc1 -verify -fsyntax-only -std=c++17 %s
+
+// expected-no-diagnostics
+
+// CWG 426 / [basic.link]p6: the same identifier with both internal and
+// external linkage should be ill-formed in C++, but Clang does not yet
+// diagnose this due to ABI break concerns. This test documents current
+// behavior.
+
+static int x;
+void test_shadow(void) {
+ int x;
+ {
+ // FIXME: Per CWG 426, this should be ill-formed because the
+ // file-scope 'x' has internal linkage but this 'extern' gets
+ // external linkage due to the local shadow.
+ extern int x;
+ }
+}
diff --git a/clang/www/c_status.html b/clang/www/c_status.html
index a909a1fb3013e..f584b0f30ac58 100644
--- a/clang/www/c_status.html
+++ b/clang/www/c_status.html
@@ -283,7 +283,7 @@ <h2 id="c2y">C2y implementation status</h2>
<tr>
<td>Slay Some Earthly Demons XI</td>
<td><a href="https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3410.pdf">N3410</a></td>
- <td class="none" align="center">No</td>
+ <td class="full" align="center">Clang 21</td>
</tr>
<tr>
<td>Slay Some Earthly Demons XII</td>
More information about the cfe-commits
mailing list