[clang] [Clang] Add -Wcounted-by-static-allocation for __counted_by FAMs in fixed-size storage (PR #211697)

via cfe-commits cfe-commits at lists.llvm.org
Fri Jul 24 04:14:04 PDT 2026


https://github.com/abhijeetsharma200 updated https://github.com/llvm/llvm-project/pull/211697

>From b18b1bc6f7f51978a191f7482b8a601f69e6dad4 Mon Sep 17 00:00:00 2001
From: Abhijeet Sharma <abhijeetsharma2002 at gmail.com>
Date: Fri, 24 Jul 2026 01:44:31 +0200
Subject: [PATCH 1/3] [Clang] Add -Wcounted-by-static-allocation for
 __counted_by FAMs in fixed-size storage (#206541)

---
 clang/docs/ReleaseNotes.md                    |  7 ++
 clang/include/clang/Basic/DiagnosticGroups.td |  1 +
 .../clang/Basic/DiagnosticSemaKinds.td        |  9 +++
 clang/include/clang/Sema/Sema.h               | 12 ++++
 clang/lib/Sema/SemaBoundsSafety.cpp           | 69 +++++++++++++++++++
 clang/lib/Sema/SemaDecl.cpp                   |  2 +
 .../Sema/attr-counted-by-static-allocation.c  | 63 +++++++++++++++++
 7 files changed, 163 insertions(+)
 create mode 100644 clang/test/Sema/attr-counted-by-static-allocation.c

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 639f7b09ae002..7b60c35ae71ea 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -153,6 +153,13 @@ features cannot lower the translation-unit ABI level;
 
 - More consistent rendering of Unicode characters in diagnostic messages.
 
+- Added `-Wcounted-by-static-allocation` (off by default) which warns when a
+  variable with static, thread, or automatic storage duration has a struct type
+  whose flexible array member is annotated with `__counted_by`. The annotation
+  promises that the count field describes the size of the trailing array, which
+  can only be kept when the object is dynamically allocated with a size derived
+  from the count. (#GH206541)
+
 - Fixed bug in `-Wdocumentation` so that it correctly handles explicit
   function template instantiations (#64087).
 
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index b7072634cccf3..5170e86f16dae 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -1870,6 +1870,7 @@ def NoDeref : DiagGroup<"noderef">;
 // -fbounds-safety and bounds annotation related warnings
 def BoundsSafetyCountedByEltTyUnknownSize :
   DiagGroup<"bounds-safety-counted-by-elt-type-unknown-size">;
+def CountedByStaticAllocation : DiagGroup<"counted-by-static-allocation">;
 
 // A group for cross translation unit static analysis related warnings.
 def CrossTU : DiagGroup<"ctu">;
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 89e2f956971b3..6bd79fa2fa6c5 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -7231,6 +7231,15 @@ def warn_counted_by_attr_elt_type_unknown_size :
   Warning<err_counted_by_attr_pointee_unknown_size.Summary>,
   InGroup<BoundsSafetyCountedByEltTyUnknownSize>;
 
+def warn_counted_by_attr_fam_static_storage : Warning<
+  "flexible array member %0 with '%1' attribute in an object with "
+  "%select{automatic|thread|static}2 storage duration; the count in %3 "
+  "cannot grow or shrink the fixed-size allocation">,
+  InGroup<CountedByStaticAllocation>, DefaultIgnore;
+def note_counted_by_intended_for_dynamic_allocation : Note<
+  "'%0' is intended for dynamically allocated objects, where the size of the "
+  "allocation can be computed from the count">;
+
 // __builtin_counted_by_ref diagnostics:
 def err_builtin_counted_by_ref_invalid_arg
     : Error<"'__builtin_counted_by_ref' argument must reference a member with "
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 8a30f6319bcef..63fde69acfd86 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -2558,6 +2558,18 @@ class Sema final : public SemaBase {
   ///
   /// \returns True iff no diagnostic where emitted, false otherwise.
   bool BoundsSafetyCheckUseOfCountAttrPtr(const Expr *E);
+
+  /// Warn when a variable definition gives static, thread, or automatic
+  /// storage duration to a struct type whose flexible array member has a
+  /// `counted_by` attribute (-Wcounted-by-static-allocation). The attribute
+  /// promises that the count field describes the size of the trailing array,
+  /// which can only be kept when the object is dynamically allocated with a
+  /// size derived from the count.
+  ///
+  /// \param VD The variable declaration to check
+  ///
+  /// \returns True iff no diagnostic were emitted, false otherwise.
+  bool BoundsSafetyCheckCountedByFAMInStaticStorage(const VarDecl *VD);
   ///@}
 
   //
diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index 066dab2f0bef2..c1dd859373172 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -412,4 +412,73 @@ bool Sema::BoundsSafetyCheckUseOfCountAttrPtr(const Expr *E) {
   return false;
 }
 
+bool Sema::BoundsSafetyCheckCountedByFAMInStaticStorage(const VarDecl *VD) {
+  if (getDiagnostics().isIgnored(diag::warn_counted_by_attr_fam_static_storage,
+                                 VD->getLocation()))
+    return true;
+
+  if (VD->isInvalidDecl())
+    return true;
+
+  // Any object with a VarDecl is a fixed-size allocation; only heap objects
+  // (which have no VarDecl) can honor the count. Diagnose once per object:
+  // skip non-defining declarations, and all but the first tentative definition.
+  if (!VD->isThisDeclarationADefinition())
+    return true;
+  for (const VarDecl *Prev = VD->getPreviousDecl(); Prev;
+       Prev = Prev->getPreviousDecl())
+    if (Prev->isThisDeclarationADefinition())
+      return true;
+
+  // Arrays of such structs are still fixed-size allocations.
+  const RecordDecl *RD =
+      Context.getBaseElementType(VD->getType())->getAsRecordDecl();
+  if (!RD || RD->isInvalidDecl() || !RD->hasFlexibleArrayMember())
+    return true;
+
+  // Find the last field, descending through anonymous struct/union members.
+  const FieldDecl *FAM = nullptr;
+  for (const FieldDecl *FD : RD->fields())
+    FAM = FD;
+  while (FAM) {
+    const RecordDecl *FieldRD = FAM->getType()->getAsRecordDecl();
+    if (!FieldRD || !FieldRD->isAnonymousStructOrUnion())
+      break;
+    FAM = nullptr;
+    for (const FieldDecl *FD : FieldRD->fields())
+      FAM = FD;
+  }
+  if (!FAM)
+    return true;
+  const auto *CATy = FAM->getType()->getAs<CountAttributedType>();
+  if (!CATy)
+    return true;
+  const FieldDecl *CountFD = FAM->findCountedByField();
+  if (!CountFD)
+    return true;
+
+  unsigned StorageKind;
+  switch (VD->getStorageDuration()) {
+  case SD_Automatic:
+    StorageKind = 0;
+    break;
+  case SD_Thread:
+    StorageKind = 1;
+    break;
+  case SD_Static:
+    StorageKind = 2;
+    break;
+  case SD_FullExpression:
+  case SD_Dynamic:
+    llvm_unreachable("variable with full-expression or dynamic storage");
+  }
+
+  Diag(VD->getLocation(), diag::warn_counted_by_attr_fam_static_storage)
+      << FAM << CATy->getAttributeName(/*WithMacroPrefix=*/true) << StorageKind
+      << CountFD;
+  Diag(FAM->getLocation(), diag::note_counted_by_intended_for_dynamic_allocation)
+      << CATy->getAttributeName(/*WithMacroPrefix=*/true);
+  return false;
+}
+
 } // namespace clang
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 7de5542e72559..be693c84f0a70 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -15380,6 +15380,8 @@ void Sema::FinalizeDeclaration(Decl *ThisDecl) {
   CheckInvalidBuiltinCountedByRef(VD->getInit(),
                                   BuiltinCountedByRefKind::Initializer);
 
+  BoundsSafetyCheckCountedByFAMInStaticStorage(VD);
+
   checkAttributesAfterMerging(*this, *VD);
 
   if (VD->isStaticLocal())
diff --git a/clang/test/Sema/attr-counted-by-static-allocation.c b/clang/test/Sema/attr-counted-by-static-allocation.c
new file mode 100644
index 0000000000000..caa89a0abd570
--- /dev/null
+++ b/clang/test/Sema/attr-counted-by-static-allocation.c
@@ -0,0 +1,63 @@
+// RUN: %clang_cc1 -fsyntax-only -Wcounted-by-static-allocation -verify %s
+// RUN: %clang_cc1 -fsyntax-only -verify=quiet %s
+// quiet-no-diagnostics
+
+#define __counted_by(f)  __attribute__((counted_by(f)))
+
+struct flex {
+  int count;
+  char fam[] __counted_by(count); // expected-note 6 {{'__counted_by' is intended for dynamically allocated objects, where the size of the allocation can be computed from the count}}
+};
+
+// Global definition with an initializer.
+struct flex global_init = {.count = 10}; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with static storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+
+// Tentative definitions are diagnosed once per object.
+struct flex tentative; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with static storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+struct flex tentative;
+
+// Thread-local storage.
+_Thread_local struct flex tls_var; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with thread storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+
+// Arrays of such structs are still fixed-size allocations.
+struct flex array_of_flex[4]; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with static storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+
+// Non-defining declarations are not diagnosed; the definition is.
+extern struct flex external_obj;
+
+// Pointers to such structs are fine; the pointee can be heap-allocated.
+struct flex *ptr;
+struct flex **ptr_ptr;
+
+// A flexible array member without a count annotation is not diagnosed.
+struct plain_flex {
+  int count;
+  char fam[];
+};
+struct plain_flex plain_global = {.count = 10};
+
+// Named nested FAM struct (GNU extension): not diagnosed.
+struct outer {
+  int x;
+  struct flex inner;
+};
+struct outer outer_global;
+
+// FAM reached through a C11 anonymous struct: diagnosed.
+struct anon_holder {
+  int count;
+  struct {
+    int pad;
+    char fam[] __counted_by(count); // expected-note {{'__counted_by' is intended for dynamically allocated objects, where the size of the allocation can be computed from the count}}
+  };
+};
+struct anon_holder anon_global; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with static storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+
+void func(void) {
+  struct flex local; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with automatic storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+  static struct flex static_local; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with static storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+  struct flex *local_ptr;
+  (void)local;
+  (void)static_local;
+  (void)local_ptr;
+}

>From b23580d211984fa7af4d32a56e746106e8312150 Mon Sep 17 00:00:00 2001
From: Abhijeet Sharma <abhijeetsharma2002 at gmail.com>
Date: Fri, 24 Jul 2026 02:14:47 +0200
Subject: [PATCH 2/3] Fixed failing linter

---
 clang/lib/Sema/SemaBoundsSafety.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index c1dd859373172..6eb479ddf7952 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -476,7 +476,8 @@ bool Sema::BoundsSafetyCheckCountedByFAMInStaticStorage(const VarDecl *VD) {
   Diag(VD->getLocation(), diag::warn_counted_by_attr_fam_static_storage)
       << FAM << CATy->getAttributeName(/*WithMacroPrefix=*/true) << StorageKind
       << CountFD;
-  Diag(FAM->getLocation(), diag::note_counted_by_intended_for_dynamic_allocation)
+  Diag(FAM->getLocation(),
+       diag::note_counted_by_intended_for_dynamic_allocation)
       << CATy->getAttributeName(/*WithMacroPrefix=*/true);
   return false;
 }

>From 9768bbb3c286ce5e7ecf9f5da671ecf27fd04ece Mon Sep 17 00:00:00 2001
From: Abhijeet Sharma <abhijeetsharma2002 at gmail.com>
Date: Fri, 24 Jul 2026 13:13:45 +0200
Subject: [PATCH 3/3] Fix review comments

---
 clang/include/clang/AST/Decl.h                |  8 ++++++
 clang/lib/AST/Decl.cpp                        | 21 ++++++++++++++
 clang/lib/Sema/SemaBoundsSafety.cpp           | 15 ++--------
 .../Sema/attr-counted-by-static-allocation.c  | 28 +++++++++++++++----
 4 files changed, 54 insertions(+), 18 deletions(-)

diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index 0a6f256afa2cc..92fd2daf28ea4 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -4407,6 +4407,14 @@ class RecordDecl : public TagDecl {
     RecordDeclBits.HasFlexibleArrayMember = V;
   }
 
+  /// Find this record's flexible array member, or null if it has none.
+  ///
+  /// A struct-typed last field (named or anonymous) is descended into, since
+  /// its flexible array member then lies at this record's tail. Unions are not
+  /// descended into, so this may return null even when hasFlexibleArrayMember()
+  /// is true.
+  const FieldDecl *findFlexibleArrayMember() const;
+
   /// Whether this is an anonymous struct or union. To be an anonymous
   /// struct or union, it must have been declared without a name and
   /// there must be no objects of this type declared, e.g.,
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index 4eaef0d87f3e5..b6c83862558f1 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -5413,6 +5413,27 @@ const FieldDecl *RecordDecl::findFirstNamedDataMember() const {
   return nullptr;
 }
 
+const FieldDecl *RecordDecl::findFlexibleArrayMember() const {
+  if (!hasFlexibleArrayMember())
+    return nullptr;
+
+  const FieldDecl *Last = nullptr;
+  for (const FieldDecl *FD : fields())
+    Last = FD;
+  while (Last) {
+    const RecordDecl *FieldRD = Last->getType()->getAsRecordDecl();
+    if (!FieldRD || FieldRD->isUnion())
+      break;
+    Last = nullptr;
+    for (const FieldDecl *FD : FieldRD->fields())
+      Last = FD;
+  }
+
+  if (Last && Last->getType()->isIncompleteArrayType())
+    return Last;
+  return nullptr;
+}
+
 unsigned RecordDecl::getODRHash() {
   if (hasODRHash())
     return RecordDeclBits.ODRHash;
diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index 6eb479ddf7952..3bb438e53be67 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -433,21 +433,10 @@ bool Sema::BoundsSafetyCheckCountedByFAMInStaticStorage(const VarDecl *VD) {
   // Arrays of such structs are still fixed-size allocations.
   const RecordDecl *RD =
       Context.getBaseElementType(VD->getType())->getAsRecordDecl();
-  if (!RD || RD->isInvalidDecl() || !RD->hasFlexibleArrayMember())
+  if (!RD || RD->isInvalidDecl())
     return true;
 
-  // Find the last field, descending through anonymous struct/union members.
-  const FieldDecl *FAM = nullptr;
-  for (const FieldDecl *FD : RD->fields())
-    FAM = FD;
-  while (FAM) {
-    const RecordDecl *FieldRD = FAM->getType()->getAsRecordDecl();
-    if (!FieldRD || !FieldRD->isAnonymousStructOrUnion())
-      break;
-    FAM = nullptr;
-    for (const FieldDecl *FD : FieldRD->fields())
-      FAM = FD;
-  }
+  const FieldDecl *FAM = RD->findFlexibleArrayMember();
   if (!FAM)
     return true;
   const auto *CATy = FAM->getType()->getAs<CountAttributedType>();
diff --git a/clang/test/Sema/attr-counted-by-static-allocation.c b/clang/test/Sema/attr-counted-by-static-allocation.c
index caa89a0abd570..f76f0fae5bd69 100644
--- a/clang/test/Sema/attr-counted-by-static-allocation.c
+++ b/clang/test/Sema/attr-counted-by-static-allocation.c
@@ -1,12 +1,12 @@
-// RUN: %clang_cc1 -fsyntax-only -Wcounted-by-static-allocation -verify %s
-// RUN: %clang_cc1 -fsyntax-only -verify=quiet %s
+// RUN: %clang_cc1 -fsyntax-only -Wcounted-by-static-allocation -Wno-gnu-variable-sized-type-not-at-end -verify %s
+// RUN: %clang_cc1 -fsyntax-only -Wno-gnu-variable-sized-type-not-at-end -verify=quiet %s
 // quiet-no-diagnostics
 
 #define __counted_by(f)  __attribute__((counted_by(f)))
 
 struct flex {
   int count;
-  char fam[] __counted_by(count); // expected-note 6 {{'__counted_by' is intended for dynamically allocated objects, where the size of the allocation can be computed from the count}}
+  char fam[] __counted_by(count); // expected-note 7 {{'__counted_by' is intended for dynamically allocated objects, where the size of the allocation can be computed from the count}}
 };
 
 // Global definition with an initializer.
@@ -36,12 +36,30 @@ struct plain_flex {
 };
 struct plain_flex plain_global = {.count = 10};
 
-// Named nested FAM struct (GNU extension): not diagnosed.
+// Nested struct whose FAM is the last field: diagnosed (the FAM is at the tail).
 struct outer {
   int x;
   struct flex inner;
 };
-struct outer outer_global;
+struct outer outer_global; // expected-warning {{flexible array member 'fam' with '__counted_by' attribute in an object with static storage duration; the count in 'count' cannot grow or shrink the fixed-size allocation}}
+
+// FAM struct not the last field, so the FAM is not at the tail: not diagnosed.
+struct middle_fam {
+  int x;
+  struct flex inner;
+  int foo;
+};
+struct middle_fam middle_global;
+
+// Unions are not descended into (no counted_by in unions): not diagnosed.
+struct union_holder {
+  int count;
+  union {
+    struct flex inner;
+    long long y;
+  };
+};
+struct union_holder union_global;
 
 // FAM reached through a C11 anonymous struct: diagnosed.
 struct anon_holder {



More information about the cfe-commits mailing list