https://github.com/Holo-xy updated https://github.com/llvm/llvm-project/pull/227774
>From e907e22da9a91a44a08495294e2feb48dcff99ad Mon Sep 17 00:00:00 2001 From: Holo-xy <[email protected]> Date: Wed, 30 Sep 2026 18:58:29 +0300 Subject: [PATCH 1/3] [clang][bounds-safety][nfc] add shared bounds attribute validation introduce shared type-shape and nesting validation for counted_by, sized_by and their nullable variants. keep declaration-specific checks separate so eager and late parsing can share the validation as support expands to parameters, return types, globals and locals. add attribute flags, declarator nesting support and a disabled bounds-safety attribute-mode hook for later integration. the helpers have no active callers, so existing behavior is unchanged. co-authored-by: rapidsna <[email protected]> --- .../clang/Basic/DiagnosticSemaKinds.td | 3 + clang/include/clang/Basic/LangOptions.h | 6 + clang/include/clang/Sema/Sema.h | 40 +++++ clang/lib/Sema/SemaBoundsSafety.cpp | 145 ++++++++++++++++++ clang/lib/Sema/SemaType.cpp | 26 ++++ 5 files changed, 220 insertions(+) diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 79f10b8240aa4..0765dedeab66d 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -7269,6 +7269,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..eedb3b85555f4 100644 --- a/clang/include/clang/Basic/LangOptions.h +++ b/clang/include/clang/Basic/LangOptions.h @@ -710,6 +710,12 @@ class LangOptions : public LangOptionsBase { return ConvergentFunctions; } + /// Return true if bounds attributes follow the -fbounds-safety programming + /// model, including its attributes-only mode. + /// + /// Always false until that model is implemented upstream. + 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 2c4986d56eede..e1101238061c6 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -2521,6 +2521,46 @@ class Sema final : public SemaBase { /// Implementations are in SemaBoundsSafety.cpp ///@{ public: + struct BoundsAttrFlags { + bool CountInBytes = false; + bool OrNull = false; + bool IsEndedBy = false; + + /// Return the flags for a bounds attribute of kind \p K. + static BoundsAttrFlags get(AttributeCommonInfo::Kind K); + }; + static BoundsAttributedType::BoundsAttrKind + getBoundsAttrKind(const BoundsAttrFlags &); + + /// Check the type shape for counted_by, sized_by and their _or_null variants. + /// The caller checks declaration context, count expressions, flexible array + /// member eligibility and nesting. + /// + /// \p Flags selects the attribute variant. For void pointers, counted_by is + /// treated as a byte count outside the -fbounds-safety attribute model. Set + /// \c Flags.CountInBytes in this case if \p UpdateFlags is true. + /// \p AttrSpelling, \p AllowRedecl and \p AttrArg are reserved for that model + /// (see LangOptions::hasBoundsSafetyAttributes). + /// + /// \returns true if the shape is valid, or false after diagnosing a rejected + /// attribute. Rejection may produce a warning rather than an error. + bool ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, + SourceRange AttrRange, + BoundsAttrFlags &Flags, + StringRef AttrSpelling = {}, + bool AllowRedecl = false, + Expr *AttrArg = nullptr, + 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. + /// + /// \returns true if valid, or false after diagnosing a rejected attribute. + bool ValidateBoundsAttrTypeForTypePosition( + QualType Ty, AttributeCommonInfo::Kind AttrKind, SourceLocation AttrLoc, + SourceRange AttrRange, unsigned PointerNestLevel, BoundsAttrFlags &Flags); + /// 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..dda72a3414f52 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) { + // Clang does not yet implement __ended_by (see BoundsSafety.md). + assert(!Flags.IsEndedBy && "ended_by is not implemented"); + return getCountAttrKind(Flags.CountInBytes, Flags.OrNull); +} + +Sema::BoundsAttrFlags Sema::BoundsAttrFlags::get(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,123 @@ enum class CountedByInvalidPointeeTypeKind { VALID, }; +/// Check type conflicts under the -fbounds-safety attribute model. +/// Return std::nullopt if validation should continue with the shared checks. +static std::optional<bool> checkBoundsAttrTypeConflictsAndMisc( + Sema &S, QualType Ty, SourceLocation AttrLoc, + const Sema::BoundsAttrFlags &Flags, StringRef AttrSpelling, + bool AllowRedecl, Expr *AttrArg) { + // The -fbounds-safety model's checks for conflicting attributes, atomic + // pointers and array shapes are not upstream yet. + return std::nullopt; +} + +bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, + SourceRange AttrRange, + BoundsAttrFlags &Flags, + StringRef AttrSpelling, bool AllowRedecl, + Expr *AttrArg, bool UpdateFlags) { + if (getLangOpts().hasBoundsSafetyAttributes()) + if (std::optional<bool> Result = checkBoundsAttrTypeConflictsAndMisc( + *this, Ty, AttrLoc, Flags, AttrSpelling, AllowRedecl, AttrArg)) + return *Result; + + BoundsAttributedType::BoundsAttrKind Kind = getBoundsAttrKind(Flags); + + // counted_by and sized_by require a 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 only support counted_by outside the -fbounds-safety attribute model. + 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; + } + + // Check the pointee or element type. + 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()) { + // GNU void pointer arithmetic treats each element as one byte, so allow + // counted_by on void pointers outside the -fbounds-safety attribute model. + if (PointeeTy->isVoidType() && !getLangOpts().hasBoundsSafetyAttributes()) { + Diag(AttrLoc, diag::ext_gnu_counted_by_void_ptr) << Kind; + Diag(AttrLoc, diag::note_gnu_counted_by_void_ptr_use_sized_by) << Kind; + if (UpdateFlags) + 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) { + // Warn for compatibility with Linux kernel code using counted_by on a FAM + // of structs with FAMs. Computing these bounds requires traversing the + // elements at runtime. + 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::ValidateBoundsAttrTypeForTypePosition( + QualType Ty, AttributeCommonInfo::Kind AttrKind, SourceLocation AttrLoc, + SourceRange AttrRange, unsigned PointerNestLevel, BoundsAttrFlags &Flags) { + Flags = BoundsAttrFlags::get(AttrKind); + + // Preserve the counted_by kind for the GNU void pointer extension to match + // the field path. The count is still interpreted as a byte size. + if (!ValidateBoundsAttrTypeShape(Ty, AttrLoc, AttrRange, Flags, + /*AttrSpelling=*/{}, /*AllowRedecl=*/false, + /*AttrArg=*/nullptr, /*UpdateFlags=*/false)) + return false; + + // Currently, only attributes at the outermost level of the declared type + // are supported. + // + // FIXME: Support indirect parameters such as: + // 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); + 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 1cb96b556971a..6455477512b62 100644 --- a/clang/lib/Sema/SemaType.cpp +++ b/clang/lib/Sema/SemaType.cpp @@ -9074,6 +9074,32 @@ static void HandleHLSLParamModifierAttr(TypeProcessingState &State, } } +/// Return the number of pointer, array and function chunks before +/// \p chunkIndex for counted_by validation. +/// +/// Chunks are ordered from the identifier out. In +/// `int *__counted_by(n) *pp`, the attributed pointer is chunk 1, enclosed by +/// the pointer at chunk 0, so its nesting level is 1. +[[maybe_unused]] static unsigned getPointerNestLevel(TypeProcessingState &state, + unsigned chunkIndex) { + unsigned pointerNestLevel = 0; + const auto &stateDeclarator = state.getDeclarator(); + assert(chunkIndex <= stateDeclarator.getNumTypeObjects()); + for (unsigned i = 0; i < chunkIndex; ++i) { + const DeclaratorChunk &TypeObject = stateDeclarator.getTypeObject(i); + switch (TypeObject.Kind) { + case DeclaratorChunk::Function: + case DeclaratorChunk::Array: + case DeclaratorChunk::Pointer: + pointerNestLevel++; + break; + default: + break; + } + } + return pointerNestLevel; +} + static void processTypeAttrs(TypeProcessingState &state, QualType &type, TypeAttrLocation TAL, const ParsedAttributesView &attrs, >From 377f9a2c8e9b6b1bb4b331afa2177db8a378f1e7 Mon Sep 17 00:00:00 2001 From: Holo-xy <[email protected]> Date: Wed, 7 Oct 2026 21:15:06 +0300 Subject: [PATCH 2/3] [clang][bounds-safety] address review hasBoundsSafetyAttributes() now follows -fexperimental-bounds-safety, and counted_by on arrays of FAM structs warns in every mode. nothing calls these helpers yet. --- clang/include/clang/Basic/LangOptions.h | 8 ++++---- clang/lib/Sema/SemaBoundsSafety.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/clang/include/clang/Basic/LangOptions.h b/clang/include/clang/Basic/LangOptions.h index eedb3b85555f4..ab09e67c3c742 100644 --- a/clang/include/clang/Basic/LangOptions.h +++ b/clang/include/clang/Basic/LangOptions.h @@ -711,10 +711,10 @@ class LangOptions : public LangOptionsBase { } /// Return true if bounds attributes follow the -fbounds-safety programming - /// model, including its attributes-only mode. - /// - /// Always false until that model is implemented upstream. - bool hasBoundsSafetyAttributes() const { return false; } + /// model. Downstream, which has the full -fbounds-safety implementation, uses + /// this predicate to gate behavior that differs from upstream. Remove it once + /// that divergence is resolved. + bool hasBoundsSafetyAttributes() const { return BoundsSafety; } /// Return true if atomicrmw operations targeting allocations in private /// memory are undefined. diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp index dda72a3414f52..bb8857b8a5ddf 100644 --- a/clang/lib/Sema/SemaBoundsSafety.cpp +++ b/clang/lib/Sema/SemaBoundsSafety.cpp @@ -146,7 +146,7 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, InvalidTypeKind = CountedByInvalidPointeeTypeKind::FUNCTION; } else if (!Flags.CountInBytes && PointeeTy->isStructureTypeWithFlexibleArrayMember()) { - if (Ty->isArrayType() && !getLangOpts().BoundsSafety) { + if (Ty->isArrayType()) { // Warn for compatibility with Linux kernel code using counted_by on a FAM // of structs with FAMs. Computing these bounds requires traversing the // elements at runtime. >From da49f768fd215a71cdac3a593507d49d82f43fdd Mon Sep 17 00:00:00 2001 From: Holo-xy <[email protected]> Date: Tue, 13 Oct 2026 09:18:16 +0300 Subject: [PATCH 3/3] [clang][bounds-safety] address review restore downstream's comments and assert, return true after the warning for arrays of FAM structs and recover from pointee errors under the model as downstream does, and drop getPointerNestLevel until a caller lands. --- clang/include/clang/Sema/Sema.h | 10 ++++-- clang/lib/Sema/SemaBoundsSafety.cpp | 56 ++++++++++++++++++++++++++--- clang/lib/Sema/SemaType.cpp | 26 -------------- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index e1101238061c6..4163bba965ac0 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -2542,8 +2542,11 @@ class Sema final : public SemaBase { /// \p AttrSpelling, \p AllowRedecl and \p AttrArg are reserved for that model /// (see LangOptions::hasBoundsSafetyAttributes). /// - /// \returns true if the shape is valid, or false after diagnosing a rejected - /// attribute. Rejection may produce a warning rather than an error. + /// \returns false after diagnosing an attribute that must be dropped. Returns + /// true if the shape is valid, after the warning for an array of structs with + /// flexible array members, and, under the -fbounds-safety attribute model, + /// after an error on a pointer, with \c Flags.CountInBytes set so that type + /// construction can recover. bool ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, SourceRange AttrRange, BoundsAttrFlags &Flags, @@ -2556,7 +2559,8 @@ class Sema final : public SemaBase { /// \p Flags from \p AttrKind. \p PointerNestLevel counts the pointer, array /// and function declarator chunks enclosing the attribute. /// - /// \returns true if valid, or false after diagnosing a rejected attribute. + /// \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); diff --git a/clang/lib/Sema/SemaBoundsSafety.cpp b/clang/lib/Sema/SemaBoundsSafety.cpp index bb8857b8a5ddf..5d1ac40817adb 100644 --- a/clang/lib/Sema/SemaBoundsSafety.cpp +++ b/clang/lib/Sema/SemaBoundsSafety.cpp @@ -130,11 +130,45 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, auto InvalidTypeKind = CountedByInvalidPointeeTypeKind::VALID; bool ShouldWarn = false; if (!Flags.CountInBytes && PointeeTy->isAlwaysIncompleteType()) { - // GNU void pointer arithmetic treats each element as one byte, so allow - // counted_by on void pointers outside the -fbounds-safety attribute model. + // 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). 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; + assert(!Ty->isArrayType() && "sized_by is not allowed on arrays"); if (UpdateFlags) Flags.CountInBytes = true; return true; @@ -147,9 +181,12 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, } else if (!Flags.CountInBytes && PointeeTy->isStructureTypeWithFlexibleArrayMember()) { if (Ty->isArrayType()) { - // Warn for compatibility with Linux kernel code using counted_by on a FAM - // of structs with FAMs. Computing these bounds requires traversing the - // elements at runtime. + // 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; @@ -161,6 +198,15 @@ bool Sema::ValidateBoundsAttrTypeShape(QualType Ty, SourceLocation AttrLoc, : diag::err_counted_by_attr_pointee_unknown_size; Diag(AttrLoc, DiagID) << SelectPtrOrArr << PointeeTy << (int)InvalidTypeKind << (ShouldWarn ? 1 : 0) << Kind << AttrRange; + if (ShouldWarn) + return true; + if (getLangOpts().hasBoundsSafetyAttributes() && !Ty->isArrayType()) { + // Under BoundsSafety, recover by switching to byte count so that + // type construction can proceed and emit follow-up diagnostics. + // We don't do this for arrays because `__sized_by` is not allowed. + Flags.CountInBytes = true; + return true; + } return false; } diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp index 6455477512b62..1cb96b556971a 100644 --- a/clang/lib/Sema/SemaType.cpp +++ b/clang/lib/Sema/SemaType.cpp @@ -9074,32 +9074,6 @@ static void HandleHLSLParamModifierAttr(TypeProcessingState &State, } } -/// Return the number of pointer, array and function chunks before -/// \p chunkIndex for counted_by validation. -/// -/// Chunks are ordered from the identifier out. In -/// `int *__counted_by(n) *pp`, the attributed pointer is chunk 1, enclosed by -/// the pointer at chunk 0, so its nesting level is 1. -[[maybe_unused]] static unsigned getPointerNestLevel(TypeProcessingState &state, - unsigned chunkIndex) { - unsigned pointerNestLevel = 0; - const auto &stateDeclarator = state.getDeclarator(); - assert(chunkIndex <= stateDeclarator.getNumTypeObjects()); - for (unsigned i = 0; i < chunkIndex; ++i) { - const DeclaratorChunk &TypeObject = stateDeclarator.getTypeObject(i); - switch (TypeObject.Kind) { - case DeclaratorChunk::Function: - case DeclaratorChunk::Array: - case DeclaratorChunk::Pointer: - pointerNestLevel++; - break; - default: - break; - } - } - return pointerNestLevel; -} - static void processTypeAttrs(TypeProcessingState &state, QualType &type, TypeAttrLocation TAL, const ParsedAttributesView &attrs, _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
