[llvm-branch-commits] [clang] [BoundsSafety][NFC] Add counted_by type-shape validation helper (PR #224557)
Yeoul Na via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Fri Sep 18 08:10:58 PDT 2026
https://github.com/rapidsna updated https://github.com/llvm/llvm-project/pull/224557
>From 065540909fd20f4e1942757d27357e04ca1cd51c Mon Sep 17 00:00:00 2001
From: Yeoul Na <yeoul_na at apple.com>
Date: Thu, 10 Sep 2026 07:30:15 -0700
Subject: [PATCH] [BoundsSafety][NFC] Add counted_by type-shape validation
helper
Introduce validateCountedByAttrType and its supporting helpers
(getCountAttrKind, getPointerNestLevel, the CountedByInvalidPointeeTypeKind
enum) plus the err_counted_by_on_nested_pointer diagnostic. This is the
single "is this type valid for a counted_by-family attribute" leaf that both
the eager type-attribute path and the late-parsed path will call.
Unused at this point -- nothing invokes it yet -- so this is NFC. The callers
are added in the following commits.
---
.../clang/Basic/DiagnosticSemaKinds.td | 3 +
clang/include/clang/Basic/LangOptions.h | 7 ++
clang/include/clang/Sema/Sema.h | 24 ++++
clang/lib/Sema/SemaBoundsSafety.cpp | 112 ++++++++++++++++++
clang/lib/Sema/SemaType.cpp | 83 +++++++++++++
5 files changed, 229 insertions(+)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 825d17f49790c..0a13fb09d8ff8 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -7266,6 +7266,9 @@ def err_builtin_counted_by_ref_invalid_use : Error<
"value returned by '__builtin_counted_by_ref' cannot be used in "
"%select{an array subscript|a binary}0 expression">;
+def err_counted_by_on_nested_pointer : Error<
+ "'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}0' attribute on nested pointer type is not allowed">;
+
let CategoryName = "ARC Semantic Issue" in {
// ARC-mode diagnostics.
diff --git a/clang/include/clang/Basic/LangOptions.h b/clang/include/clang/Basic/LangOptions.h
index 7539e000d03f9..0d615824ecc5b 100644
--- a/clang/include/clang/Basic/LangOptions.h
+++ b/clang/include/clang/Basic/LangOptions.h
@@ -710,6 +710,13 @@ class LangOptions : public LangOptionsBase {
return ConvergentFunctions;
}
+ /// Returns true when the -fbounds-safety attribute programming model is in
+ /// effect. There is no attributes-only mode on this base, so this is always
+ /// false; it exists so the shared Sema::ValidateBoundsAttrTypeShape leaf can
+ /// gate its -fbounds-safety-only branches with the same predicate used
+ /// downstream (where those branches carry the extra diagnostics).
+ bool hasBoundsSafetyAttributes() const { return false; }
+
/// Return true if atomicrmw operations targeting allocations in private
/// memory are undefined.
bool threadPrivateMemoryAtomicsAreUndefined() const {
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 46df36396bc6f..8533f2823ed14 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -2493,6 +2493,30 @@ class Sema final : public SemaBase {
/// Implementations are in SemaBoundsSafety.cpp
///@{
public:
+ struct BoundsAttrFlags {
+ bool CountInBytes = false;
+ bool OrNull = false;
+ bool IsEndedBy = false;
+ };
+ static BoundsAttrFlags getBoundsAttrFlags(AttributeCommonInfo::Kind K);
+ static BoundsAttributedType::BoundsAttrKind
+ getBoundsAttrKind(const BoundsAttrFlags &);
+
+ /// Validates that a type is eligible for an "externally counted" bounds
+ /// attribute (counted_by/sized_by and their _or_null variants).
+ ///
+ /// \p Flags selects the attribute variant. \returns true if the type is
+ /// valid, false on error (diagnostics emitted). For `void *__counted_by(n)`
+ /// it warns that the count is treated as a byte size and sets
+ /// \p Flags.CountInBytes; callers that want to preserve a counted_by node
+ /// pass a scratch copy (see validateBoundsAttrTypeForTypePosition).
+ bool ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc,
+ SourceRange AttrRange,
+ BoundsAttrFlags &Flags,
+ StringRef AttrSpelling = {},
+ bool AllowRedecl = false,
+ Expr *AttrArg = nullptr);
+
/// Check if applying the specified attribute variant from the "counted by"
/// family of attributes to FieldDecl \p FD is semantically valid. If
/// semantically invalid diagnostics will be emitted explaining the problems.
diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index 75041c801b6ff..2afe0812dcf4f 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -26,6 +26,34 @@ static CountAttributedType::BoundsAttrKind getCountAttrKind(bool CountInBytes,
: CountAttributedType::CountedBy;
}
+BoundsAttributedType::BoundsAttrKind
+Sema::getBoundsAttrKind(const BoundsAttrFlags &Flags) {
+ // `ended_by` (Flags.IsEndedBy) has no home on this base; the field exists for
+ // struct parity with the downstream API but is never set here.
+ return getCountAttrKind(Flags.CountInBytes, Flags.OrNull);
+}
+
+Sema::BoundsAttrFlags Sema::getBoundsAttrFlags(AttributeCommonInfo::Kind K) {
+ BoundsAttrFlags Flags;
+ switch (K) {
+ case ParsedAttr::AT_SizedBy:
+ Flags.CountInBytes = true;
+ break;
+ case ParsedAttr::AT_SizedByOrNull:
+ Flags.CountInBytes = true;
+ Flags.OrNull = true;
+ break;
+ case ParsedAttr::AT_CountedBy:
+ break;
+ case ParsedAttr::AT_CountedByOrNull:
+ Flags.OrNull = true;
+ break;
+ default:
+ llvm_unreachable("unexpected bounds attribute kind");
+ }
+ return Flags;
+}
+
static const RecordDecl *GetEnclosingNamedOrTopAnonRecord(const FieldDecl *FD) {
const auto *RD = FD->getParent();
// An unnamed struct is treated as anonymous struct at this point.
@@ -49,6 +77,90 @@ enum class CountedByInvalidPointeeTypeKind {
VALID,
};
+bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc,
+ SourceRange AttrRange,
+ BoundsAttrFlags &Flags,
+ StringRef AttrSpelling, bool AllowRedecl,
+ Expr *AttrArg) {
+ // The downstream leaf runs a `hasBoundsSafetyAttributes()`-gated
+ // `checkBoundsAttrTypeConflictsAndMisc` preamble and an `ended_by` early
+ // path; both depend on machinery (`DynamicRangePointerType`,
+ // `ValueTerminatedType`, the `err_bounds_safety_*` diagnostics) that does not
+ // exist here, so they are the omitted bounds-safety arms. The rest matches.
+ BoundsAttributedType::BoundsAttrKind Kind = getBoundsAttrKind(Flags);
+
+ // counted_by/sized_by: must be pointer or array.
+ if (!Ty->isPointerType() && !Ty->isArrayType()) {
+ Diag(AttrLoc, diag::err_count_attr_not_on_ptr_or_flexible_array_member)
+ << Kind << 0;
+ return false;
+ }
+
+ // Arrays with sized_by or _or_null variants are not allowed under the
+ // non -fbounds-safety path; emit the "did you mean to use 'counted_by'" hint.
+ if (!getLangOpts().hasBoundsSafetyAttributes() && Ty->isArrayType() &&
+ (Flags.CountInBytes || Flags.OrNull)) {
+ Diag(AttrLoc, diag::err_count_attr_not_on_ptr_or_flexible_array_member)
+ << Kind << /*suggest counted_by*/ 1;
+ return false;
+ }
+
+ // Pointee/element type validation.
+ QualType PointeeTy;
+ int SelectPtrOrArr;
+ if (Ty->isPointerType()) {
+ PointeeTy = Ty->getPointeeType();
+ SelectPtrOrArr = 0;
+ } else {
+ const ArrayType *AT = getASTContext().getAsArrayType(Ty);
+ PointeeTy = AT->getElementType();
+ SelectPtrOrArr = 1;
+ }
+
+ auto InvalidTypeKind = CountedByInvalidPointeeTypeKind::VALID;
+ bool ShouldWarn = false;
+ if (!Flags.CountInBytes && PointeeTy->isAlwaysIncompleteType()) {
+ // Exception: void has an implicit size of 1 byte for pointer arithmetic
+ // (following GNU convention). Therefore, counted_by on void* is allowed
+ // and behaves equivalently to sized_by (treating the count as bytes).
+ if (PointeeTy->isVoidType() && !getLangOpts().hasBoundsSafetyAttributes()) {
+ // Emit a warning that this is a GNU extension.
+ Diag(AttrLoc, diag::ext_gnu_counted_by_void_ptr) << Kind;
+ Diag(AttrLoc, diag::note_gnu_counted_by_void_ptr_use_sized_by) << Kind;
+ Flags.CountInBytes = true;
+ return true;
+ }
+ InvalidTypeKind = CountedByInvalidPointeeTypeKind::INCOMPLETE;
+ } else if (PointeeTy->isSizelessType()) {
+ InvalidTypeKind = CountedByInvalidPointeeTypeKind::SIZELESS;
+ } else if (PointeeTy->isFunctionType()) {
+ InvalidTypeKind = CountedByInvalidPointeeTypeKind::FUNCTION;
+ } else if (!Flags.CountInBytes &&
+ PointeeTy->isStructureTypeWithFlexibleArrayMember()) {
+ if (Ty->isArrayType() && !getLangOpts().BoundsSafety) {
+ // This is a workaround for the Linux kernel that has already adopted
+ // `counted_by` on a FAM where the pointee is a struct with a FAM. This
+ // should be an error because computing the bounds of the array cannot
+ // be done correctly without manually traversing every struct object in
+ // the array at runtime. To allow the code to be built this error is
+ // downgraded to a warning.
+ ShouldWarn = true;
+ }
+ InvalidTypeKind = CountedByInvalidPointeeTypeKind::FLEXIBLE_ARRAY_MEMBER;
+ }
+
+ if (InvalidTypeKind != CountedByInvalidPointeeTypeKind::VALID) {
+ unsigned DiagID = ShouldWarn
+ ? diag::warn_counted_by_attr_elt_type_unknown_size
+ : diag::err_counted_by_attr_pointee_unknown_size;
+ Diag(AttrLoc, DiagID) << SelectPtrOrArr << PointeeTy << (int)InvalidTypeKind
+ << (ShouldWarn ? 1 : 0) << Kind << AttrRange;
+ return false;
+ }
+
+ return true;
+}
+
bool Sema::CheckCountedByAttrOnField(FieldDecl *FD, Expr *E, bool CountInBytes,
bool OrNull) {
// Check the context the attribute is used in
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index 1026e5e938137..ae9a9a260e624 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -9041,6 +9041,89 @@ static void HandleHLSLParamModifierAttr(TypeProcessingState &State,
}
}
+static CountAttributedType::BoundsAttrKind getCountAttrKind(bool CountInBytes,
+ bool OrNull) {
+ if (CountInBytes)
+ return OrNull ? CountAttributedType::SizedByOrNull
+ : CountAttributedType::SizedBy;
+ return OrNull ? CountAttributedType::CountedByOrNull
+ : CountAttributedType::CountedBy;
+}
+
+/// Calculate the pointer nesting level for counted_by attribute validation.
+/// Counts the number of pointer/array/function declarator chunks before the
+/// specified chunk index.
+///
+/// For example, given \c "int * __counted_by(n) *pp" the declarator chunks
+/// are (outermost first): [0]=Pointer(\c int **), [1]=Pointer(\c int *).
+/// When processing the inner pointer at \p chunkIndex=1, one Pointer chunk
+/// precedes it, so the function returns 1.
+///
+/// \param state The type processing state
+/// \param chunkIndex The index of the current declarator chunk
+/// \return The number of pointer/array/function chunks before chunkIndex
+static unsigned getPointerNestLevel(TypeProcessingState &state,
+ unsigned chunkIndex) {
+ unsigned pointerNestLevel = 0;
+ const auto &stateDeclarator = state.getDeclarator();
+ assert(chunkIndex <= stateDeclarator.getNumTypeObjects());
+ // DeclChunks are ordered identifier out. Index 0 is the outer most type
+ // object. Find outer pointer, array or function.
+ for (unsigned i = 0; i < chunkIndex; ++i) {
+ auto TypeObject = stateDeclarator.getTypeObject(i);
+ switch (TypeObject.Kind) {
+ case DeclaratorChunk::Function:
+ case DeclaratorChunk::Array:
+ case DeclaratorChunk::Pointer:
+ pointerNestLevel++;
+ break;
+ default:
+ break;
+ }
+ }
+ return pointerNestLevel;
+}
+
+/// The single "is this type valid for a counted_by-family attribute in type
+/// position" leaf, shared by the eager path (HandleCountedByAttrOnType) and the
+/// late-parsed path (Sema::ActOnLateParsedTypeAttr).
+///
+/// Delegates the type-shape checks to Sema::ValidateBoundsAttrTypeShape so
+/// there is exactly one copy of those diagnostics, and adds the nested-pointer
+/// rejection that only applies in type position.
+///
+/// \p Flags is set from \p AttrKind and returned to the caller for building the
+/// type.
+static bool validateBoundsAttrTypeForTypePosition(
+ Sema &S, QualType Ty, ParsedAttr::Kind AttrKind, SourceLocation AttrLoc,
+ SourceRange AttrRange, unsigned PointerNestLevel,
+ Sema::BoundsAttrFlags &Flags) {
+ Flags = Sema::getBoundsAttrFlags(AttrKind);
+
+ // ValidateBoundsAttrTypeShape may rewrite Flags.CountInBytes: for
+ // `void *__counted_by(n)` it warns "treated as 'sized_by'" and sets
+ // CountInBytes so the -fbounds-safety path builds a SizedBy node. The
+ // pre-existing non-BoundsSafety field path discards that rewrite and builds a
+ // CountedBy node, so absorb it in a scratch copy to keep this
+ // diagnostics-only.
+ //
+ // FIXME: Reconcile with the -fbounds-safety path, which honors the rewrite.
+ Sema::BoundsAttrFlags Scratch = Flags;
+ if (!S.ValidateBoundsAttrTypeShape(Ty, AttrLoc, AttrRange, Scratch))
+ return false;
+
+ // A counted_by-family attribute has to end up at the outermost level of the
+ // declared type; a nested one would be buried where the bounds cannot be
+ // maintained.
+ if (PointerNestLevel > 0) {
+ S.Diag(AttrLoc, diag::err_counted_by_on_nested_pointer)
+ << Sema::getBoundsAttrKind(Flags);
+ return false;
+ }
+
+ return true;
+}
+
static void processTypeAttrs(TypeProcessingState &state, QualType &type,
TypeAttrLocation TAL,
const ParsedAttributesView &attrs,
More information about the llvm-branch-commits
mailing list