[llvm-branch-commits] [clang] [BoundsSafety] Handle the counted_by family as a type attribute (PR #233582)

Yeoul Na via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Wed Oct 21 11:46:04 PDT 2026


https://github.com/rapidsna updated https://github.com/llvm/llvm-project/pull/233582

>From 90e03eac21072fba55ffb7de10271cdb4e53252b Mon Sep 17 00:00:00 2001
From: Yeoul Na <yeoul_na at apple.com>
Date: Fri, 18 Sep 2026 00:28:21 -0700
Subject: [PATCH 1/3] [BoundsSafety] Handle the counted_by family as a type
 attribute

counted_by / sized_by (and their _or_null variants) were handled as a
declaration-position attribute through handleCountedByAttrField.

Build the type during type construction instead and properly handle as a
type-position attribute. Building the node in type position also allows
the new way of late-parsing for type attributes: creates a type node
in place and fills it in once late parsing is done, which will be
enabled in the follow-up commits.

Assisted-by: Opus 5.5
---
 .../clang/Basic/DiagnosticSemaKinds.td        |   2 +
 clang/include/clang/Sema/Sema.h               |  18 ++-
 clang/lib/Sema/SemaBoundsSafety.cpp           | 149 ++++--------------
 clang/lib/Sema/SemaDecl.cpp                   |  19 +++
 clang/lib/Sema/SemaDeclAttr.cpp               |  48 +++---
 clang/lib/Sema/SemaType.cpp                   | 110 ++++++++++++-
 .../AST/attr-counted-by-or-null-struct-ptrs.c |  23 ---
 clang/test/AST/attr-counted-by-struct-ptrs.c  |  26 ---
 .../AST/attr-sized-by-or-null-struct-ptrs.c   |  24 ---
 clang/test/AST/attr-sized-by-struct-ptrs.c    |  24 ---
 ...ruct-ptrs-completable-incomplete-pointee.c |  13 +-
 .../attr-counted-by-or-null-struct-ptrs.c     |  16 +-
 clang/test/Sema/attr-counted-by-return-type.c |  53 +++++++
 ...ruct-ptrs-completable-incomplete-pointee.c |   9 +-
 clang/test/Sema/attr-counted-by-struct-ptrs.c |  16 +-
 .../Sema/attr-sized-by-or-null-struct-ptrs.c  |  16 +-
 clang/test/Sema/attr-sized-by-struct-ptrs.c   |  16 +-
 17 files changed, 307 insertions(+), 275 deletions(-)
 create mode 100644 clang/test/Sema/attr-counted-by-return-type.c

diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index a23667f1fb1c2c..d796386cf8a174 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -7281,6 +7281,8 @@ def note_counted_by_counter_field : Note<
 
 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">;
+def err_counted_by_on_return_type : Error<
+  "'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}0' attribute cannot be applied to a function return type">;
 
 let CategoryName = "ARC Semantic Issue" in {
 
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 9082042c48502d..9a3810efabd133 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -2556,14 +2556,26 @@ class Sema final : public SemaBase {
                                    bool UpdateFlags = true);
 
   /// Check a counted_by-family attribute in type position and initialize
-  /// \p Flags from \p AttrKind. \p PointerNestLevel counts the pointer, array
-  /// and function declarator chunks enclosing the attribute.
+  /// \p Flags from \p AttrKind. \p PointerNestLevel counts the pointer and
+  /// array declarator chunks enclosing the attribute, up to the nearest
+  /// function chunk. \p OnReturnType is true if a function chunk encloses it,
+  /// i.e. the attribute is in a function's return type.
   ///
   /// \returns false after diagnosing an attribute that must be dropped (see
   /// ValidateBoundsAttrTypeShape).
   bool ValidateBoundsAttrTypeForTypePosition(
       QualType Ty, AttributeCommonInfo::Kind AttrKind, SourceLocation AttrLoc,
-      SourceRange AttrRange, unsigned PointerNestLevel, BoundsAttrFlags &Flags);
+      SourceRange AttrRange, unsigned PointerNestLevel, bool OnReturnType,
+      BoundsAttrFlags &Flags);
+
+  /// Check where a counted_by-family attribute of kind \p Kind sits in its
+  /// declarator, as described by \p PointerNestLevel and \p OnReturnType (see
+  /// ValidateBoundsAttrTypeForTypePosition).
+  ///
+  /// \returns false after diagnosing an attribute in an unsupported position.
+  bool ValidateBoundsAttrPosition(BoundsAttributedType::BoundsAttrKind Kind,
+                                  SourceLocation AttrLoc, SourceRange AttrRange,
+                                  unsigned PointerNestLevel, bool OnReturnType);
 
   /// Check if applying the specified attribute variant from the "counted by"
   /// family of attributes to FieldDecl \p FD is semantically valid. If
diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index cebc680297cd9c..a4bb0548a58b98 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -213,15 +213,18 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc,
   return true;
 }
 
-bool Sema::ValidateBoundsAttrTypeForTypePosition(
-    QualType Ty, AttributeCommonInfo::Kind AttrKind, SourceLocation AttrLoc,
-    SourceRange AttrRange, unsigned PointerNestLevel, BoundsAttrFlags &Flags) {
-  Flags = BoundsAttrFlags::get(AttrKind);
-
-  if (!ValidateBoundsAttrTypeShape(Ty, AttrLoc, AttrRange, Flags,
-                                   /*AttrSpelling=*/{}, /*AllowRedecl=*/false,
-                                   /*AttrArg=*/nullptr, /*UpdateFlags=*/false))
+bool Sema::ValidateBoundsAttrPosition(BoundsAttributedType::BoundsAttrKind Kind,
+                                      SourceLocation AttrLoc,
+                                      SourceRange AttrRange,
+                                      unsigned PointerNestLevel,
+                                      bool OnReturnType) {
+  // FIXME: Support function return types, with a count that refers to the
+  // function's parameters. Their nesting is then checked below, relative to
+  // the return type.
+  if (OnReturnType) {
+    Diag(AttrLoc, diag::err_counted_by_on_return_type) << Kind << AttrRange;
     return false;
+  }
 
   // Currently, only attributes at the outermost level of the declared type
   // are supported.
@@ -230,14 +233,31 @@ bool Sema::ValidateBoundsAttrTypeForTypePosition(
   //   void f(int *__counted_by(*len) *buf, int *len);
   // See https://github.com/llvm/llvm-project/issues/166411.
   if (PointerNestLevel > 0) {
-    Diag(AttrLoc, diag::err_counted_by_on_nested_pointer)
-        << getBoundsAttrKind(Flags);
+    Diag(AttrLoc, diag::err_counted_by_on_nested_pointer) << Kind;
     return false;
   }
 
   return true;
 }
 
+bool Sema::ValidateBoundsAttrTypeForTypePosition(
+    QualType Ty, AttributeCommonInfo::Kind AttrKind, SourceLocation AttrLoc,
+    SourceRange AttrRange, unsigned PointerNestLevel, bool OnReturnType,
+    BoundsAttrFlags &Flags) {
+  Flags = BoundsAttrFlags::get(AttrKind);
+
+  if (!ValidateBoundsAttrTypeShape(Ty, AttrLoc, AttrRange, Flags,
+                                   /*AttrSpelling=*/{}, /*AllowRedecl=*/false,
+                                   /*AttrArg=*/nullptr, /*UpdateFlags=*/false))
+    return false;
+
+  if (!ValidateBoundsAttrPosition(getBoundsAttrKind(Flags), AttrLoc, AttrRange,
+                                  PointerNestLevel, OnReturnType))
+    return false;
+
+  return true;
+}
+
 bool Sema::CheckCountedByAttrOnField(FieldDecl *FD, Expr *E, bool CountInBytes,
                                      bool OrNull) {
   // Check the context the attribute is used in
@@ -250,20 +270,7 @@ bool Sema::CheckCountedByAttrOnField(FieldDecl *FD, Expr *E, bool CountInBytes,
     return true;
   }
 
-  const auto FieldTy = FD->getType();
-  if (FieldTy->isArrayType() && (CountInBytes || OrNull)) {
-    Diag(FD->getBeginLoc(),
-         diag::err_count_attr_not_on_ptr_or_flexible_array_member)
-        << Kind << FD->getLocation() << /* suggest counted_by */ 1;
-    return true;
-  }
-  if (!FieldTy->isArrayType() && !FieldTy->isPointerType()) {
-    Diag(FD->getBeginLoc(),
-         diag::err_count_attr_not_on_ptr_or_flexible_array_member)
-        << Kind << FD->getLocation() << /* do not suggest counted_by */ 0;
-    return true;
-  }
-
+  const QualType FieldTy = FD->getType();
   LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel =
       LangOptions::StrictFlexArraysLevelKind::IncompleteOnly;
   if (FieldTy->isArrayType() &&
@@ -275,98 +282,7 @@ bool Sema::CheckCountedByAttrOnField(FieldDecl *FD, Expr *E, bool CountInBytes,
     return true;
   }
 
-  CountedByInvalidPointeeTypeKind InvalidTypeKind =
-      CountedByInvalidPointeeTypeKind::VALID;
-  QualType PointeeTy;
-  int SelectPtrOrArr = 0;
-  if (FieldTy->isPointerType()) {
-    PointeeTy = FieldTy->getPointeeType();
-    SelectPtrOrArr = 0;
-  } else {
-    assert(FieldTy->isArrayType());
-    const ArrayType *AT = getASTContext().getAsArrayType(FieldTy);
-    PointeeTy = AT->getElementType();
-    SelectPtrOrArr = 1;
-  }
-  // Note: The `Decl::isFlexibleArrayMemberLike` check earlier on means
-  // only `PointeeTy->isStructureTypeWithFlexibleArrayMember()` is reachable
-  // when `FieldTy->isArrayType()`.
-  bool ShouldWarn = false;
-  if (!CountInBytes && PointeeTy->isAlwaysIncompleteType()) {
-    // In general using `counted_by` or `counted_by_or_null` on
-    // pointers where the pointee is an incomplete type are problematic. This is
-    // because it isn't possible to compute the pointer's bounds without knowing
-    // the pointee type size. At the same time it is common to forward declare
-    // types in header files.
-    //
-    // E.g.:
-    //
-    // struct Handle;
-    // struct Wrapper {
-    //   size_t count;
-    //   struct Handle* __counted_by(count) handles;
-    // }
-    //
-    // To allow the above code pattern but still prevent the pointee type from
-    // being incomplete in places where bounds checks are needed the following
-    // scheme is used:
-    //
-    // * When the pointee type might not always be an incomplete type (i.e.
-    // a type that is currently incomplete but might be completed later
-    // on in the translation unit) the attribute is allowed by this method
-    // but later uses of the FieldDecl are checked that the pointee type
-    // is complete see `BoundsSafetyCheckAssignmentToCountAttrPtr`,
-    // `BoundsSafetyCheckInitialization`, and
-    // `BoundsSafetyCheckUseOfCountAttrPtr`
-    //
-    // * When the pointee type is always an incomplete type (e.g.
-    // `void` in strict C mode) the attribute is disallowed by this method
-    // because we know the type can never be completed so there's no reason
-    // to allow it.
-    //
-    // 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).
-    bool IsVoidPtr = PointeeTy->isVoidType();
-    if (IsVoidPtr) {
-      // Emit a warning that this is a GNU extension.
-      Diag(FD->getBeginLoc(), diag::ext_gnu_counted_by_void_ptr) << Kind;
-      Diag(FD->getBeginLoc(), diag::note_gnu_counted_by_void_ptr_use_sized_by)
-          << Kind;
-      assert(InvalidTypeKind == CountedByInvalidPointeeTypeKind::VALID);
-    } else {
-      InvalidTypeKind = CountedByInvalidPointeeTypeKind::INCOMPLETE;
-    }
-  } else if (PointeeTy->isSizelessType()) {
-    InvalidTypeKind = CountedByInvalidPointeeTypeKind::SIZELESS;
-  } else if (PointeeTy->isFunctionType()) {
-    InvalidTypeKind = CountedByInvalidPointeeTypeKind::FUNCTION;
-  } else if (!CountInBytes &&
-             PointeeTy->isStructureTypeWithFlexibleArrayMember()) {
-    if (FieldTy->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(FD->getBeginLoc(), DiagID)
-        << SelectPtrOrArr << PointeeTy << (int)InvalidTypeKind
-        << (ShouldWarn ? 1 : 0) << Kind << FD->getSourceRange();
-    return true;
-  }
-
-  // Check the expression
-
+  // Validate the expression type
   if (!E->getType()->isIntegerType() || E->getType()->isBooleanType()) {
     Diag(E->getBeginLoc(), diag::err_count_attr_argument_not_integer)
         << Kind << E->getSourceRange();
@@ -381,6 +297,7 @@ bool Sema::CheckCountedByAttrOnField(FieldDecl *FD, Expr *E, bool CountInBytes,
     return true;
   }
 
+  // Validate count field references
   auto *CountDecl = DRE->getDecl();
   FieldDecl *CountFD = dyn_cast<FieldDecl>(CountDecl);
   if (auto *IFD = dyn_cast<IndirectFieldDecl>(CountDecl)) {
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 21e279ec2000c3..157216a50f5555 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -20243,6 +20243,25 @@ void Sema::ActOnFields(Scope *S, SourceLocation RecLoc, Decl *EnclosingDecl,
     }
   }
 
+  if (!getLangOpts().ExperimentalLateParseAttributes) {
+    // Perform FieldDecl-dependent validation for counted_by family attributes.
+    for (auto *D : Fields) {
+      FieldDecl *FD = cast<FieldDecl>(D);
+      if (auto *CAT = FD->getType()->getAs<CountAttributedType>()) {
+        if (CheckCountedByAttrOnField(FD, CAT->getCountExpr(),
+                                      CAT->isCountInBytes(), CAT->isOrNull())) {
+          // Rejected. Strip the CountAttributedType so the field keeps its
+          // plain wrapped type. The pre-refactor eager path built the type only
+          // after this check passed, so on failure no CAT ever existed.
+          QualType Wrapped = CAT->desugar();
+          FD->setType(Wrapped);
+          FD->setTypeSourceInfo(
+              Context.getTrivialTypeSourceInfo(Wrapped, FD->getLocation()));
+        }
+      }
+    }
+  }
+
   // Verify that all the fields are okay.
   SmallVector<FieldDecl*, 32> RecFields;
   const FieldDecl *PreviousField = nullptr;
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index 33b680c4d93d4c..717fa57691d502 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -7109,41 +7109,35 @@ static void handleNoPFPAttrField(Sema &S, Decl *D, const ParsedAttr &AL) {
   D->addAttr(NoFieldProtectionAttr::Create(S.Context, AL));
 }
 
+// Only reached for a counted_by-family attribute that
+// -fexperimental-late-parse-attributes late-parses as a declaration attribute.
+// This will be removed once late-parsing for type attribute is fully switched
+// over to the new mechanism so it's properly supported.
+// Every other one is a type attribute, handled by HandleCountedByAttrOnType.
 static void handleCountedByAttrField(Sema &S, Decl *D, const ParsedAttr &AL) {
+  // Already applied, or diagnosed, as a type attribute.
+  if (AL.isUsedAsTypeAttr())
+    return;
+
   auto *CountExpr = AL.getArgAsExpr(0);
   if (!CountExpr)
     return;
 
-  bool CountInBytes;
-  bool OrNull;
-  switch (AL.getKind()) {
-  case ParsedAttr::AT_CountedBy:
-    CountInBytes = false;
-    OrNull = false;
-    break;
-  case ParsedAttr::AT_CountedByOrNull:
-    CountInBytes = false;
-    OrNull = true;
-    break;
-  case ParsedAttr::AT_SizedBy:
-    CountInBytes = true;
-    OrNull = false;
-    break;
-  case ParsedAttr::AT_SizedByOrNull:
-    CountInBytes = true;
-    OrNull = true;
-    break;
-  default:
-    llvm_unreachable("unexpected counted_by family attribute");
-  }
-
-  FieldDecl *FD = cast<FieldDecl>(D);
-  if (S.CheckCountedByAttrOnField(FD, CountExpr, CountInBytes, OrNull))
+  auto *FD = cast<FieldDecl>(D);
+  Sema::BoundsAttrFlags Flags = Sema::BoundsAttrFlags::get(AL.getKind());
+  if (!S.ValidateBoundsAttrTypeShape(FD->getType(), AL.getLoc(), AL.getRange(),
+                                     Flags, /*AttrSpelling=*/{},
+                                     /*AllowRedecl=*/false, /*AttrArg=*/nullptr,
+                                     /*UpdateFlags=*/false))
+    return;
+  if (S.CheckCountedByAttrOnField(FD, CountExpr, Flags.CountInBytes,
+                                  Flags.OrNull))
     return;
 
   QualType CAT = S.BuildCountAttributedArrayOrPointerType(
-      FD->getType(), CountExpr, CountInBytes, OrNull);
-  FD->setType(CAT);
+      FD->getType(), CountExpr, Flags.CountInBytes, Flags.OrNull);
+  if (!CAT.isNull())
+    FD->setType(CAT);
 }
 
 static void handleFunctionReturnThunksAttr(Sema &S, Decl *D,
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index af0403e73480c0..76325c9be203ef 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -6455,6 +6455,7 @@ GetTypeSourceInfoForDeclarator(TypeProcessingState &State,
         break;
       }
 
+      case TypeLoc::CountAttributed:
       case TypeLoc::Adjusted:
       case TypeLoc::BTFTagAttributed: {
         CurrTL = CurrTL.getNextTypeLoc().getUnqualifiedLoc();
@@ -9072,6 +9073,85 @@ static void HandleHLSLParamModifierAttr(TypeProcessingState &State,
   }
 }
 
+/// Return true if the type built at \p chunkIndex is part of a function's
+/// return type, i.e. a function chunk wraps it at any level. In both
+/// `int *__counted_by(n) (*fp)(void)` and `int *__counted_by(n) *(*fp)(void)`,
+/// the attributed pointer is in the return type of the function `fp` points to.
+///
+/// Chunk 0 is the one closest to the identifier, which is the outermost part of
+/// the type, so the type built by chunk i is wrapped by chunk i - 1. A
+/// parameter has its own declarator, so it is never part of \p D's chunks.
+static bool isOnFunctionReturnType(const Declarator &D, unsigned chunkIndex) {
+  assert(chunkIndex <= D.getNumTypeObjects());
+  for (unsigned i = chunkIndex; i != 0; --i)
+    if (D.getTypeObject(i - 1).Kind == DeclaratorChunk::Function)
+      return true;
+  return false;
+}
+
+/// Return the number of pointer and array chunks that wrap the type built at
+/// \p chunkIndex, up to the nearest function chunk. The level is relative to
+/// the type the attribute belongs to: the declared type, or the return type of
+/// the nearest enclosing function.
+///
+/// In `int *__counted_by(n) *pp`, the attributed pointer is chunk 1, wrapped by
+/// the pointer at chunk 0, so its level is 1. In
+/// `int *__counted_by(n) *(*fp)(void)`, the attributed pointer is chunk 4,
+/// wrapped by the pointer at chunk 3 and then the function at chunk 2, so its
+/// level is 1, relative to the return type.
+static unsigned getPointerNestLevel(const Declarator &D, unsigned chunkIndex) {
+  assert(chunkIndex <= D.getNumTypeObjects());
+  unsigned Level = 0;
+  for (unsigned i = chunkIndex; i != 0; --i) {
+    switch (D.getTypeObject(i - 1).Kind) {
+    case DeclaratorChunk::Function:
+      return Level;
+    case DeclaratorChunk::Pointer:
+    case DeclaratorChunk::Array:
+      ++Level;
+      break;
+    default:
+      break;
+    }
+  }
+  return Level;
+}
+
+static void HandleCountedByAttrOnType(TypeProcessingState &State,
+                                      QualType &CurType, ParsedAttr &Attr) {
+  Sema &S = State.getSema();
+
+  // This attribute is only supported in C.
+  if (!Attr.diagnoseLangOpts(S)) {
+    Attr.setInvalid();
+    return;
+  }
+
+  auto *CountExpr = Attr.getArgAsExpr(0);
+  if (!CountExpr)
+    return;
+
+  const Declarator &D = State.getDeclarator();
+  unsigned chunkIndex = State.getCurrentChunkIndex();
+
+  Sema::BoundsAttrFlags Flags;
+  if (!S.ValidateBoundsAttrTypeForTypePosition(
+          CurType, Attr.getKind(), Attr.getLoc(), Attr.getRange(),
+          getPointerNestLevel(D, chunkIndex),
+          isOnFunctionReturnType(D, chunkIndex), Flags)) {
+    Attr.setInvalid();
+    return;
+  }
+
+  QualType NewType = S.BuildCountAttributedArrayOrPointerType(
+      CurType, CountExpr, Flags.CountInBytes, Flags.OrNull);
+  if (NewType.isNull()) {
+    Attr.setInvalid();
+    return;
+  }
+  CurType = NewType;
+}
+
 bool Sema::ProcessLateParsedTypeAttr(LateParsedTypeAttrInfo &LA, QualType &QT) {
   // A DeclSpec attribute runs this once per declarator, as in
   // `IP __counted_by(n) a, b;`. Reuse one node, as the eager path does, so no
@@ -9085,7 +9165,8 @@ bool Sema::ProcessLateParsedTypeAttr(LateParsedTypeAttrInfo &LA, QualType &QT) {
   BoundsAttrFlags Flags;
   if (!ValidateBoundsAttrTypeForTypePosition(QT, LA.Kind, LA.NameLoc,
                                              SourceRange(LA.NameLoc),
-                                             /*PointerNestLevel=*/0, Flags))
+                                             /*PointerNestLevel=*/0,
+                                             /*OnReturnType=*/false, Flags))
     return false;
 
   // The argument isn't parsed yet, so build the type without a count.
@@ -9335,6 +9416,19 @@ static void processTypeAttrs(TypeProcessingState &state, QualType &type,
       break;
     }
 
+    case ParsedAttr::AT_CountedBy:
+    case ParsedAttr::AT_CountedByOrNull:
+    case ParsedAttr::AT_SizedBy:
+    case ParsedAttr::AT_SizedByOrNull: {
+      // XXX: The late-parsing path hasn't been switched to the new mechanism to
+      // properly support type-position attributes yet.
+      if (!state.getSema().getLangOpts().ExperimentalLateParseAttributes) {
+        HandleCountedByAttrOnType(state, type, attr);
+        attr.setUsedAsTypeAttr();
+      }
+      break;
+    }
+
     MS_TYPE_ATTRS_CASELIST:
       if (!handleMSPointerTypeQualifierAttr(state, attr, type))
         attr.setUsedAsTypeAttr();
@@ -10106,7 +10200,19 @@ QualType Sema::BuildCountAttributedArrayOrPointerType(QualType WrappedTy,
                                                       Expr *CountExpr,
                                                       bool CountInBytes,
                                                       bool OrNull) {
-  assert(WrappedTy->isIncompleteArrayType() || WrappedTy->isPointerType());
+  // Accept any array or pointer type here. For arrays, validation that it's
+  // a flexible array member is deferred until CheckCountedByAttrOnField.
+  assert(WrappedTy->isArrayType() || WrappedTy->isPointerType());
+
+  // Reject non-DeclRefExpr early to avoid cast failure in
+  // BuildTypeCoupledDecls.
+  if (!isa<DeclRefExpr>(CountExpr)) {
+    unsigned Kind = getBoundsAttrKind(BoundsAttrFlags{CountInBytes, OrNull});
+    Diag(CountExpr->getBeginLoc(),
+         diag::err_count_attr_only_support_simple_decl_reference)
+        << Kind << CountExpr->getSourceRange();
+    return QualType();
+  }
 
   llvm::SmallVector<TypeCoupledDeclRefInfo, 1> Decls;
   BuildTypeCoupledDecls(CountExpr, Decls);
diff --git a/clang/test/AST/attr-counted-by-or-null-struct-ptrs.c b/clang/test/AST/attr-counted-by-or-null-struct-ptrs.c
index d42547003f0b38..ca603402f0a345 100644
--- a/clang/test/AST/attr-counted-by-or-null-struct-ptrs.c
+++ b/clang/test/AST/attr-counted-by-or-null-struct-ptrs.c
@@ -56,29 +56,6 @@ struct on_member_pointer_complete_ty_ty_pos {
   struct size_known *__counted_by_or_null(count) buf;
 };
 
-// TODO: This should be forbidden but isn't due to counted_by_or_null being treated as a
-// declaration attribute. The attribute ends up on the outer most pointer
-// (allowed by sema) even though syntactically its supposed to be on the inner
-// pointer (would not allowed by sema due to pointee being a function type).
-// CHECK-LABEL: RecordDecl {{.+}} struct on_member_pointer_fn_ptr_ty_ty_pos_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} fn_ptr 'void (** __counted_by_or_null(count))(void)':'void (**)(void)'
-struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
-  int count;
-  void (* __counted_by_or_null(count) * fn_ptr)(void);
-};
-
-// FIXME: The generated AST here is wrong. The attribute should be on the inner
-// pointer.
-// CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __counted_by_or_null(count)':'struct size_known **'
-struct on_nested_pointer_inner {
-  int count;
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__counted_by_or_null` can only be nested when used in function parameters.
-  struct size_known *__counted_by_or_null(count) *buf;
-};
 
 // CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_outer definition
 // CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
diff --git a/clang/test/AST/attr-counted-by-struct-ptrs.c b/clang/test/AST/attr-counted-by-struct-ptrs.c
index afef9c8c3b95d2..414a7007c7b49d 100644
--- a/clang/test/AST/attr-counted-by-struct-ptrs.c
+++ b/clang/test/AST/attr-counted-by-struct-ptrs.c
@@ -45,8 +45,6 @@ struct on_pointer_anon_count {
 //==============================================================================
 // __counted_by on struct member pointer in type attribute position
 //==============================================================================
-// TODO: Correctly parse counted_by as a type attribute. Currently it is parsed
-// as a declaration attribute
 
 // CHECK-LABEL: RecordDecl {{.+}} struct on_member_pointer_complete_ty_ty_pos definition
 // CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
@@ -56,30 +54,6 @@ struct on_member_pointer_complete_ty_ty_pos {
   struct size_known *__counted_by(count) buf;
 };
 
-// TODO: This should be forbidden but isn't due to counted_by being treated as a
-// declaration attribute. The attribute ends up on the outer most pointer
-// (allowed by sema) even though syntactically its supposed to be on the inner
-// pointer (would not allowed by sema due to pointee being a function type).
-// CHECK-LABEL: RecordDecl {{.+}} struct on_member_pointer_fn_ptr_ty_ty_pos_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} fn_ptr 'void (** __counted_by(count))(void)':'void (**)(void)'
-struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
-  int count;
-  void (* __counted_by(count) * fn_ptr)(void);
-};
-
-// FIXME: The generated AST here is wrong. The attribute should be on the inner
-// pointer.
-// CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __counted_by(count)':'struct size_known **'
-struct on_nested_pointer_inner {
-  int count;
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__counted_by` can only be nested when used in function parameters.
-  struct size_known *__counted_by(count) *buf;
-};
-
 // CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_outer definition
 // CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
 // CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __counted_by(count)':'struct size_known **'
diff --git a/clang/test/AST/attr-sized-by-or-null-struct-ptrs.c b/clang/test/AST/attr-sized-by-or-null-struct-ptrs.c
index 7273280e4b60c0..2592d0fd5cb4e5 100644
--- a/clang/test/AST/attr-sized-by-or-null-struct-ptrs.c
+++ b/clang/test/AST/attr-sized-by-or-null-struct-ptrs.c
@@ -56,30 +56,6 @@ struct on_member_pointer_complete_ty_ty_pos {
   struct size_known *__sized_by_or_null(count) buf;
 };
 
-// TODO: This should be forbidden but isn't due to sized_by_or_null being treated as a
-// declaration attribute. The attribute ends up on the outer most pointer
-// (allowed by sema) even though syntactically its supposed to be on the inner
-// pointer (would not allowed by sema due to pointee being a function type).
-// CHECK-LABEL: RecordDecl {{.+}} struct on_member_pointer_fn_ptr_ty_ty_pos_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} fn_ptr 'void (** __sized_by_or_null(count))(void)':'void (**)(void)'
-struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
-  int count;
-  void (* __sized_by_or_null(count) * fn_ptr)(void);
-};
-
-// FIXME: The generated AST here is wrong. The attribute should be on the inner
-// pointer.
-// CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __sized_by_or_null(count)':'struct size_known **'
-struct on_nested_pointer_inner {
-  int count;
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__sized_by_or_null` can only be nested when used in function parameters.
-  struct size_known *__sized_by_or_null(count) *buf;
-};
-
 // CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_outer definition
 // CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
 // CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __sized_by_or_null(count)':'struct size_known **'
diff --git a/clang/test/AST/attr-sized-by-struct-ptrs.c b/clang/test/AST/attr-sized-by-struct-ptrs.c
index 738eaf8cbf36b2..4d7797fa823956 100644
--- a/clang/test/AST/attr-sized-by-struct-ptrs.c
+++ b/clang/test/AST/attr-sized-by-struct-ptrs.c
@@ -56,30 +56,6 @@ struct on_member_pointer_complete_ty_ty_pos {
   struct size_known *__sized_by(count) buf;
 };
 
-// TODO: This should be forbidden but isn't due to sized_by being treated as a
-// declaration attribute. The attribute ends up on the outer most pointer
-// (allowed by sema) even though syntactically its supposed to be on the inner
-// pointer (would not allowed by sema due to pointee being a function type).
-// CHECK-LABEL: RecordDecl {{.+}} struct on_member_pointer_fn_ptr_ty_ty_pos_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} fn_ptr 'void (** __sized_by(count))(void)':'void (**)(void)'
-struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
-  int count;
-  void (* __sized_by(count) * fn_ptr)(void);
-};
-
-// FIXME: The generated AST here is wrong. The attribute should be on the inner
-// pointer.
-// CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_inner definition
-// CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
-// CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __sized_by(count)':'struct size_known **'
-struct on_nested_pointer_inner {
-  int count;
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__sized_by` can only be nested when used in function parameters.
-  struct size_known *__sized_by(count) *buf;
-};
-
 // CHECK-LABEL: RecordDecl {{.+}} struct on_nested_pointer_outer definition
 // CHECK-NEXT:  |-FieldDecl {{.+}} referenced count 'int'
 // CHECK-NEXT:  `-FieldDecl {{.+}} buf 'struct size_known ** __sized_by(count)':'struct size_known **'
diff --git a/clang/test/Sema/attr-counted-by-or-null-struct-ptrs-completable-incomplete-pointee.c b/clang/test/Sema/attr-counted-by-or-null-struct-ptrs-completable-incomplete-pointee.c
index cff5a14c70b993..955e781b5679b3 100644
--- a/clang/test/Sema/attr-counted-by-or-null-struct-ptrs-completable-incomplete-pointee.c
+++ b/clang/test/Sema/attr-counted-by-or-null-struct-ptrs-completable-incomplete-pointee.c
@@ -1,5 +1,5 @@
-// RUN: %clang_cc1 -fsyntax-only -verify %s
-// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
+// RUN: %clang_cc1 -fsyntax-only -verify=expected,immediate %s
+// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify=expected,late %s
 
 #define __counted_by_or_null(f)  __attribute__((counted_by_or_null(f)))
 
@@ -616,10 +616,11 @@ struct IncompleteTy3;
 
 struct CBBufFAMofCountedByPtrs {
   int size;
-  // TODO: This is misleading. The attribute is written in the type position
-  // but clang currently doesn't treat it like that and it gets treated as
-  // an attribute on the array, rather than on the element type.
-  // expected-error at +1{{'counted_by_or_null' only applies to pointers; did you mean to use 'counted_by'?}}
+  // The attribute is written on the element type, so it's a nested pointer.
+  // With -fexperimental-late-parse-attributes it's still treated as an
+  // attribute on the array until late parsing handles type attributes.
+  // late-error at +2{{'counted_by_or_null' only applies to pointers; did you mean to use 'counted_by'?}}
+  // immediate-error at +1{{'counted_by_or_null' attribute on nested pointer type is not allowed}}
   struct IncompleteTy3* __counted_by_or_null(size) arr[];
 };
 
diff --git a/clang/test/Sema/attr-counted-by-or-null-struct-ptrs.c b/clang/test/Sema/attr-counted-by-or-null-struct-ptrs.c
index 0fd739ca7d4c34..1bd625b0329cc6 100644
--- a/clang/test/Sema/attr-counted-by-or-null-struct-ptrs.c
+++ b/clang/test/Sema/attr-counted-by-or-null-struct-ptrs.c
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -Wpointer-arith -verify %s
+// RUN: %clang_cc1 -fsyntax-only -Wpointer-arith -verify=expected,immediate %s
 // RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -Wpointer-arith -verify %s
 
 #define __counted_by_or_null(f)  __attribute__((counted_by_or_null(f)))
@@ -158,10 +158,13 @@ struct on_member_pointer_fn_ptr_ty_ty_pos {
   fn_ptr_ty __counted_by_or_null(count) fn_ptr;
 };
 
-// TODO: This should be forbidden but isn't due to counted_by_or_null being treated
-// as a declaration attribute.
+// Forbidden: the pointee is a function type. With
+// -fexperimental-late-parse-attributes it's still treated as a declaration
+// attribute on the outermost pointer until late parsing handles type
+// attributes.
 struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
   int count;
+  // immediate-error at +1{{'counted_by_or_null' cannot be applied to a pointer with pointee of unknown size because 'void (void)' is a function type}}
   void (* __counted_by_or_null(count) * fn_ptr)(void);
 };
 
@@ -181,9 +184,12 @@ struct on_member_pointer_struct_with_annotated_vla_ty_pos {
 };
 
 struct on_nested_pointer_inner {
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__counted_by_or_null` can only be nested when used in function parameters.
+  // Nested bounds attributes are not allowed. With
+  // -fexperimental-late-parse-attributes it's still treated as a declaration
+  // attribute on the outermost pointer until late parsing handles type
+  // attributes.
   int count;
+  // immediate-error at +1{{'counted_by_or_null' attribute on nested pointer type is not allowed}}
   struct size_known *__counted_by_or_null(count) *buf;
 };
 
diff --git a/clang/test/Sema/attr-counted-by-return-type.c b/clang/test/Sema/attr-counted-by-return-type.c
new file mode 100644
index 00000000000000..6b9fe33a76853d
--- /dev/null
+++ b/clang/test/Sema/attr-counted-by-return-type.c
@@ -0,0 +1,53 @@
+// RUN: %clang_cc1 -fblocks -fsyntax-only -verify %s
+
+// counted_by-family attributes aren't supported on function return types,
+// including the return type of the function a function-pointer or
+// block-pointer member points to. They're rejected with a dedicated diagnostic.
+// They used to be reported as a nested pointer, because the function chunk
+// counted as a level of pointer nesting.
+
+#define __counted_by(f) __attribute__((counted_by(f)))
+#define __sized_by(f) __attribute__((sized_by(f)))
+#define __counted_by_or_null(f) __attribute__((counted_by_or_null(f)))
+#define __sized_by_or_null(f) __attribute__((sized_by_or_null(f)))
+
+extern int g;
+
+// expected-error at +1{{'counted_by' attribute cannot be applied to a function return type}}
+int *__counted_by(g) fn(void);
+// expected-error at +1{{'sized_by' attribute cannot be applied to a function return type}}
+char *__sized_by(4) fn_const(void);
+
+struct fn_ptr_return {
+  int n;
+  // expected-error at +1{{'counted_by' attribute cannot be applied to a function return type}}
+  int *__counted_by(n) (*fp)(void);
+};
+
+struct block_ptr_return {
+  int n;
+  // expected-error at +1{{'counted_by_or_null' attribute cannot be applied to a function return type}}
+  int *__counted_by_or_null(n) (^bp)(void);
+};
+
+struct fn_ptr_array_return {
+  int n;
+  // expected-error at +1{{'sized_by_or_null' attribute cannot be applied to a function return type}}
+  char *__sized_by_or_null(n) (*fpa[2])(void);
+};
+
+// A counted pointer nested anywhere in a return type is rejected as being on
+// the return type. Its nesting is measured relative to the return type.
+struct fn_ptr_return_nested {
+  int n;
+  // expected-error at +1{{'counted_by' attribute cannot be applied to a function return type}}
+  int *__counted_by(n) *(*fp)(void);
+};
+
+// Nesting stops at the function: the pointer to the function and the array
+// holding it don't count toward the return type's nesting.
+struct fn_ptr_array_nested_return {
+  int n;
+  // expected-error at +1{{'counted_by' attribute cannot be applied to a function return type}}
+  int *__counted_by(n) *(*fpa[2])(void);
+};
diff --git a/clang/test/Sema/attr-counted-by-struct-ptrs-completable-incomplete-pointee.c b/clang/test/Sema/attr-counted-by-struct-ptrs-completable-incomplete-pointee.c
index d28a2086b51b88..073c039b3810a4 100644
--- a/clang/test/Sema/attr-counted-by-struct-ptrs-completable-incomplete-pointee.c
+++ b/clang/test/Sema/attr-counted-by-struct-ptrs-completable-incomplete-pointee.c
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -fsyntax-only -verify=expected,immediate %s
 // RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
 
 #define __counted_by(f)  __attribute__((counted_by(f)))
@@ -616,9 +616,10 @@ struct IncompleteTy3;
 
 struct CBBufFAMofCountedByPtrs {
   int size;
-  // TODO: This is misleading. The attribute is written in the type position
-  // but clang currently doesn't treat it like that and it gets treated as
-  // an attribute on the array, rather than on the element type.
+  // The attribute is written on the element type, so it's a nested pointer.
+  // With -fexperimental-late-parse-attributes it's still treated as an
+  // attribute on the array until late parsing handles type attributes.
+  // immediate-error at +1{{'counted_by' attribute on nested pointer type is not allowed}}
   struct IncompleteTy3* __counted_by(size) arr[];
 };
 
diff --git a/clang/test/Sema/attr-counted-by-struct-ptrs.c b/clang/test/Sema/attr-counted-by-struct-ptrs.c
index a42f3895695a32..cfa9d10dba78b0 100644
--- a/clang/test/Sema/attr-counted-by-struct-ptrs.c
+++ b/clang/test/Sema/attr-counted-by-struct-ptrs.c
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -Wpointer-arith -verify %s
+// RUN: %clang_cc1 -fsyntax-only -Wpointer-arith -verify=expected,immediate %s
 // RUN: %clang_cc1 -fsyntax-only -Wpointer-arith -fexperimental-late-parse-attributes %s -verify
 
 #define __counted_by(f)  __attribute__((counted_by(f)))
@@ -157,10 +157,13 @@ struct on_member_pointer_fn_ptr_ty_ty_pos {
   fn_ptr_ty __counted_by(count) fn_ptr;
 };
 
-// TODO: This should be forbidden but isn't due to counted_by being treated
-// as a declaration attribute.
+// Forbidden: the pointee is a function type. With
+// -fexperimental-late-parse-attributes it's still treated as a declaration
+// attribute on the outermost pointer until late parsing handles type
+// attributes.
 struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
   int count;
+  // immediate-error at +1{{'counted_by' cannot be applied to a pointer with pointee of unknown size because 'void (void)' is a function type}}
   void (* __counted_by(count) * fn_ptr)(void);
 };
 
@@ -180,9 +183,12 @@ struct on_member_pointer_struct_with_annotated_vla_ty_pos {
 };
 
 struct on_nested_pointer_inner {
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__counted_by` can only be nested when used in function parameters.
+  // Nested bounds attributes are not allowed. With
+  // -fexperimental-late-parse-attributes it's still treated as a declaration
+  // attribute on the outermost pointer until late parsing handles type
+  // attributes.
   int count;
+  // immediate-error at +1{{'counted_by' attribute on nested pointer type is not allowed}}
   struct size_known *__counted_by(count) *buf;
 };
 
diff --git a/clang/test/Sema/attr-sized-by-or-null-struct-ptrs.c b/clang/test/Sema/attr-sized-by-or-null-struct-ptrs.c
index 82819e5cf43fff..201d34c4d20676 100644
--- a/clang/test/Sema/attr-sized-by-or-null-struct-ptrs.c
+++ b/clang/test/Sema/attr-sized-by-or-null-struct-ptrs.c
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -fsyntax-only -verify=expected,immediate %s
 // RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
 
 #define __sized_by_or_null(f)  __attribute__((sized_by_or_null(f)))
@@ -149,10 +149,13 @@ struct on_member_pointer_fn_ptr_ty_ty_pos {
   fn_ptr_ty __sized_by_or_null(size) fn_ptr;
 };
 
-// TODO: This should be forbidden but isn't due to sized_by_or_null being treated
-// as a declaration attribute.
+// Forbidden: the pointee is a function type. With
+// -fexperimental-late-parse-attributes it's still treated as a declaration
+// attribute on the outermost pointer until late parsing handles type
+// attributes.
 struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
   int size;
+  // immediate-error at +1{{'sized_by_or_null' cannot be applied to a pointer with pointee of unknown size because 'void (void)' is a function type}}
   void (* __sized_by_or_null(size) * fn_ptr)(void);
 };
 
@@ -167,9 +170,12 @@ struct on_member_pointer_struct_with_annotated_vla_ty_pos {
 };
 
 struct on_nested_pointer_inner {
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__sized_by_or_null` can only be nested when used in function parameters.
+  // Nested bounds attributes are not allowed. With
+  // -fexperimental-late-parse-attributes it's still treated as a declaration
+  // attribute on the outermost pointer until late parsing handles type
+  // attributes.
   int size;
+  // immediate-error at +1{{'sized_by_or_null' attribute on nested pointer type is not allowed}}
   struct size_known *__sized_by_or_null(size) *buf;
 };
 
diff --git a/clang/test/Sema/attr-sized-by-struct-ptrs.c b/clang/test/Sema/attr-sized-by-struct-ptrs.c
index 6dc76b8bc7081e..adfa8d82f7d906 100644
--- a/clang/test/Sema/attr-sized-by-struct-ptrs.c
+++ b/clang/test/Sema/attr-sized-by-struct-ptrs.c
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -fsyntax-only -verify=expected,immediate %s
 // RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
 
 #define __sized_by(f)  __attribute__((sized_by(f)))
@@ -149,10 +149,13 @@ struct on_member_pointer_fn_ptr_ty_ty_pos {
   fn_ptr_ty __sized_by(size) fn_ptr;
 };
 
-// TODO: This should be forbidden but isn't due to sized_by being treated
-// as a declaration attribute.
+// Forbidden: the pointee is a function type. With
+// -fexperimental-late-parse-attributes it's still treated as a declaration
+// attribute on the outermost pointer until late parsing handles type
+// attributes.
 struct on_member_pointer_fn_ptr_ty_ty_pos_inner {
   int size;
+  // immediate-error at +1{{'sized_by' cannot be applied to a pointer with pointee of unknown size because 'void (void)' is a function type}}
   void (* __sized_by(size) * fn_ptr)(void);
 };
 
@@ -167,9 +170,12 @@ struct on_member_pointer_struct_with_annotated_vla_ty_pos {
 };
 
 struct on_nested_pointer_inner {
-  // TODO: This should be disallowed because in the `-fbounds-safety` model
-  // `__sized_by` can only be nested when used in function parameters.
+  // Nested bounds attributes are not allowed. With
+  // -fexperimental-late-parse-attributes it's still treated as a declaration
+  // attribute on the outermost pointer until late parsing handles type
+  // attributes.
   int size;
+  // immediate-error at +1{{'sized_by' attribute on nested pointer type is not allowed}}
   struct size_known *__sized_by(size) *buf;
 };
 

>From efbe6027d791eab27256d0b87c37d37b60e2fd09 Mon Sep 17 00:00:00 2001
From: Yeoul Na <yeoul_na at apple.com>
Date: Tue, 20 Oct 2026 10:49:52 -0700
Subject: [PATCH 2/3] [BoundsSafety] Reject a second bounds attribute on the
 same pointer

A pointer with two counted_by-family attributes, as in
`int *__counted_by(n) __sized_by(m) p;`, got one CountAttributedType stacked on
another. Reject the second attribute with
err_counted_by_on_bounds_attributed_type, which names both attributes.

Assisted-by: Opus 5.5
---
 .../clang/Basic/DiagnosticSemaKinds.td        |  4 ++
 clang/lib/Sema/SemaBoundsSafety.cpp           | 13 +++++
 .../attr-counted-by-multiple-on-pointer.c     | 58 +++++++++++++++++++
 3 files changed, 75 insertions(+)
 create mode 100644 clang/test/Sema/attr-counted-by-multiple-on-pointer.c

diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index d796386cf8a174..197c81bb45d53f 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -7283,6 +7283,10 @@ 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">;
 def err_counted_by_on_return_type : Error<
   "'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}0' attribute cannot be applied to a function return type">;
+def err_counted_by_on_bounds_attributed_type : Error<
+  "'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}0' attribute "
+  "cannot be applied to a pointer that already has a "
+  "'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}1' attribute">;
 
 let CategoryName = "ARC Semantic Issue" in {
 
diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index a4bb0548a58b98..b187410bb6956f 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -255,6 +255,19 @@ bool Sema::ValidateBoundsAttrTypeForTypePosition(
                                   PointerNestLevel, OnReturnType))
     return false;
 
+  // A pointer takes only one bounds attribute. This sees one applied eagerly
+  // or late, since a late-parsed one is an incomplete CountAttributedType until
+  // its count is parsed.
+  //
+  // TODO: Converge with the -fbounds-safety attribute model checks in
+  // checkBoundsAttrTypeConflictsAndMisc.
+  if (!getLangOpts().hasBoundsSafetyAttributes())
+    if (const auto *CATy = Ty->getAs<CountAttributedType>()) {
+      Diag(AttrLoc, diag::err_counted_by_on_bounds_attributed_type)
+          << getBoundsAttrKind(Flags) << CATy->getKind() << AttrRange;
+      return false;
+    }
+
   return true;
 }
 
diff --git a/clang/test/Sema/attr-counted-by-multiple-on-pointer.c b/clang/test/Sema/attr-counted-by-multiple-on-pointer.c
new file mode 100644
index 00000000000000..be588c15bd1648
--- /dev/null
+++ b/clang/test/Sema/attr-counted-by-multiple-on-pointer.c
@@ -0,0 +1,58 @@
+// RUN: %clang_cc1 -fsyntax-only -verify %s
+
+// A pointer can have only one counted_by-family attribute. A second one used
+// to stack another CountAttributedType on the first.
+
+#define __counted_by(f) __attribute__((counted_by(f)))
+#define __counted_by_or_null(f) __attribute__((counted_by_or_null(f)))
+#define __sized_by(f) __attribute__((sized_by(f)))
+#define __sized_by_or_null(f) __attribute__((sized_by_or_null(f)))
+
+typedef int *IP;
+
+struct same_attr {
+  int n;
+  int m;
+  // expected-error at +1{{'counted_by' attribute cannot be applied to a pointer that already has a 'counted_by' attribute}}
+  int *__counted_by(n) __counted_by(m) p;
+};
+
+struct same_count {
+  int n;
+  // expected-error at +1{{'counted_by' attribute cannot be applied to a pointer that already has a 'counted_by' attribute}}
+  int *__counted_by(n) __counted_by(n) p;
+};
+
+struct different_attrs {
+  int n;
+  int m;
+  // expected-error at +1{{'sized_by' attribute cannot be applied to a pointer that already has a 'counted_by' attribute}}
+  int *__counted_by(n) __sized_by(m) p;
+  // expected-error at +1{{'counted_by_or_null' attribute cannot be applied to a pointer that already has a 'sized_by_or_null' attribute}}
+  int *__sized_by_or_null(n) __counted_by_or_null(m) q;
+};
+
+// One in declaration-specifier position, shared by both declarators, is
+// diagnosed once.
+struct shared_decl_spec {
+  int n;
+  int m;
+  // expected-error at +1{{'sized_by' attribute cannot be applied to a pointer that already has a 'counted_by' attribute}}
+  IP __counted_by(n) __sized_by(m) a, b;
+};
+
+// One in declaration-specifier position and one in the declarator.
+struct decl_spec_and_declarator {
+  int n;
+  int m;
+  // expected-error at +1{{'sized_by' attribute cannot be applied to a pointer that already has a 'counted_by' attribute}}
+  IP __counted_by(n) a __sized_by(m);
+};
+
+// One in the pointer chunk and one after the declarator.
+struct chunk_and_after_declarator {
+  int n;
+  int m;
+  // expected-error at +1{{'sized_by' attribute cannot be applied to a pointer that already has a 'counted_by' attribute}}
+  int *__counted_by(n) p __sized_by(m);
+};

>From 8513c826de45e9171bcb72689ccdea6b2c291709 Mon Sep 17 00:00:00 2001
From: Yeoul Na <yeoul_na at apple.com>
Date: Tue, 20 Oct 2026 18:00:11 -0700
Subject: [PATCH 3/3] Fix test failures in -fexperimental-bounds-safety

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

diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp
index b187410bb6956f..c4477c228a08ac 100644
--- a/clang/lib/Sema/SemaBoundsSafety.cpp
+++ b/clang/lib/Sema/SemaBoundsSafety.cpp
@@ -164,7 +164,7 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc,
     // 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()) {
+    if (PointeeTy->isVoidType()) {
       // 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;
@@ -180,7 +180,7 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc,
     InvalidTypeKind = CountedByInvalidPointeeTypeKind::FUNCTION;
   } else if (!Flags.CountInBytes &&
              PointeeTy->isStructureTypeWithFlexibleArrayMember()) {
-    if (Ty->isArrayType()) {
+    if (!getLangOpts().BoundsSafety && Ty->isArrayType()) {
       // 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



More information about the llvm-branch-commits mailing list